Anıl Şenocak Anıl Şenocak
Yazılım Mühendisi
Cover image
2026-07-11 0 toplam yorum
🎯Distributed Rate Limiter for Spring Boot with Kotlin and Redis

Distributed Rate Limiter for Spring Boot with Kotlin and Redis

It provides easy-to-use, production-ready distributed rate limiting for your Spring Boot services. It supports multiple strategies (per IP, per user, custom keys), is highly configurable, and integrates seamlessly with Spring Boot via auto-configuration.

The Problem

Modern applications face several challenges that make rate limiting necessary:

  • Resource Protection: Without rate limits, a single client can consume disproportionate resources, affecting service availability for others.
  • API Abuse Prevention: Malicious users may attempt to overwhelm your service with requests, causing denial of service.
  • Cost Control: Each API call consumes computing resources and potentially third-party services that may charge per request.
  • Data Consistency: Rapid, repeated write operations can cause race conditions and data inconsistency.

Traditional in-memory rate limiters work well for single-instance applications but fall short in distributed environments where multiple service instances handle requests independently.

Features

  • Distributed rate limiting using Redis (standalone, cluster or sentinel)
  • Configurable rate limits via properties or annotations
  • Support multiple strategies: per IP, per user or custom keys
  • Customizable error responses
  • Easy integration with Spring Boot applications via auto-configuration
  • Java 21+ and Spring Boot 3.4.4+ support

Requirements

  • Java 21 or higher
  • Spring Boot 3.4.4 or higher
  • Redis server

The Solution

Distributed Rate Limiting This library implements a distributed rate limiting solution using Redis as a centralized counter store. Here’s how the core logic works in the actual implementation:

@Service  
class RateLimiterService(  
    private val redisTemplate: RedisTemplate<String, Any>,  
    private val properties: RateLimiterProperties  
) {  
    fun isAllowed(key: String, limit: Int = properties.defaultLimit, duration: Long = properties.defaultDuration): Boolean {  
        if (!properties.enabled)  
            return true  
        val now: Long = Instant.now().epochSecond  
        val redisKey = "${properties.keyPrefix}$key"  
        val operations = redisTemplate.opsForZSet()  
        // Remove expired entries  
        operations.removeRangeByScore(redisKey, 0.0, (now - duration).toDouble())  
        // Count current entries  
        val currentCount: Long = operations.size(redisKey) ?: 0  
        // Check if limit is reached  
        if (currentCount >= limit)  
            return false  
        // Add new entry  
        operations.add(redisKey, now.toString(), now.toDouble())  
        // Set expiration for the key  
        redisTemplate.expire(redisKey, duration + 1, TimeUnit.SECONDS)  
        return true  
    }  
}

The aspect that enforces rate limits on annotated methods:

@Aspect  
@Component  
class RateLimitAspect(  
    private val rateLimiterService: RateLimiterService,  
    private val properties: RateLimiterProperties  
) {  
    private val parser: ExpressionParser = SpelExpressionParser()  
  
    @Before("@annotation(com.github.senocak.rate.annotation.RateLimit) || @within(com.github.senocak.rate.annotation.RateLimit)")  
    fun checkRateLimit(joinPoint: JoinPoint) {  
        if (!properties.enabled)  
            return  
        val signature: MethodSignature = joinPoint.signature as MethodSignature  
        val method: Method = signature.method  
  
        // Get the RateLimit annotation from method or class  
        val rateLimit: RateLimit = method.getAnnotation(RateLimit::class.java)  
            ?: method.declaringClass.getAnnotation(RateLimit::class.java)  
            ?: return  
  
        // Get limit and duration from annotation or use defaults  
        val limit: Int = if (rateLimit.limit > 0) rateLimit.limit else properties.defaultLimit  
        val duration: Long = if (rateLimit.duration > 0) rateLimit.duration else properties.defaultDuration  
  
        // Evaluate the key expression  
        val key: String = evaluateKey(keyExpression = rateLimit.key, joinPoint = joinPoint)  
  
        // Check if the request is allowed  
        if (!rateLimiterService.isAllowed(key = key, limit = limit, duration = duration)) {  
            // Calculate the actual remaining seconds until the rate limit resets  
            val remainingSeconds: Long = rateLimiterService.getRemainingSeconds(key = key, duration = duration)  
            throw RateLimitExceededException(  
                message = "Rate limit exceeded. Try again in $remainingSeconds seconds.",  
                remainingSeconds = remainingSeconds,  
                limit = limit  
            )  
        }  
    }  
}

How It Works

  • Distributed Coordination: Redis serves as the central source of truth, ensuring all service instances apply the same rate limits.
  • Sliding Window Algorithm: The implementation uses Redis sorted sets (ZSets) to track requests within a specific time window. Expired entries are automatically removed.
  • Dynamic Expression-Based Keys: The aspect supports SpEL expressions to create dynamic rate limit keys based on request parameters, headers, or authentication details.
  • Graceful Degradation: If Redis is unavailable, the system can be configured to either allow all requests or fall back to a local rate limiter.

Usage

Rate limiting can be applied with a simple annotation. Use the @RateLimit annotation on controllers or methods:

// Default rate limiting (from properties)  
@GetMapping("/api/default")  
@RateLimit  
fun defaultRateLimit(): Map<String, String> {  
    return mapOf("message" to "This endpoint uses default rate limiting")  
}  
  
// Custom rate limiting (5 requests per 30 seconds)  
@GetMapping("/api/custom")  
@RateLimit(limit = 5, duration = 30)  
fun customRateLimit(): Map<String, String> {  
    return mapOf("message" to "This endpoint uses custom rate limiting")  
}  
  
// Rate limiting per user ID  
@GetMapping("/api/user/{userId}")  
@RateLimit(key = "'user:' + #userId")  
fun userRateLimit(@PathVariable userId: String): Map<String, String> {  
    return mapOf("message" to "This endpoint uses rate limiting per user ID")  
}

Configuration

Configure your rate limiter through Spring Boot properties:

spring:  
  data:  
    redis:  
      host: localhost  
      port: 6379  
      database: 0  
      timeout: 60000  
  
rate-limiter:  
  enabled: true  
  default-limit: 10  
  default-duration: 60  
  key-prefix: "rate\_limit:"  
  fallback-strategy: DENY  \# DENY or ALLOW

Rate Limiter Properties

rate-limiter.enabled — Enable/disable rate limiting — true rate-limiter.default-limit — Default number of requests allowed — 10 rate-limiter.default-duration — Default time window in seconds — 60 rate-limiter.key-prefix — Prefix for Redis keys — "rate_limit:"

Auto-Configuration

The library uses Spring Boot’s auto-configuration mechanism to automatically set up the rate limiter components:

  1. RateLimiterAutoConfiguration is automatically detected by Spring Boot
  2. It configures the necessary beans (RateLimiterService, RateLimitAspect) if they don’t already exist
  3. It imports the Redis configuration and enables the RateLimiterProperties
  4. The auto-configuration is conditional on:
  • Redis being available
  • The rate-limiter.enabled property being true (or not specified)

Custom Keys with SpEL

The key parameter of the @RateLimit annotation supports Spring Expression Language (SpEL) for dynamic key generation:

  • **#request.getRemoteAddr()**Client IP address (default)
  • **#request.getHeader("X-Forwarded-For")** IP from X-Forwarded-For header
  • **'user:'+#userId**User ID from path variable
  • **#user.username**Username from authenticated user

Key Innovations

  • Sliding Window Precision: Unlike simple counter-based implementations, this solution provides a true sliding window that accurately tracks requests over time.
  • Informative Responses: When a rate limit is exceeded, the service can include headers with details about the limit, remaining requests, and reset time.
  • Flexible Key Generation: The SpEL expression support makes it easy to create rate limits based on complex combinations of request attributes.
  • Low Overhead: The Redis operations are optimized to minimize latency impact on API requests.

Conclusion

Rate limiting is essential infrastructure for any production API. This implementation provides a robust, distributed solution that can be easily integrated into Spring Boot applications using Kotlin.

By centralizing the rate limiting logic in Redis, we ensure consistent enforcement across all service instances while maintaining high performance and reliability.

Source Code

Full code example can be found in github

Yorumlar
Henüz yorum yok. İlk yorumu sen yaz.
Yorum Bırak
Adınız (isteğe bağlı)
Yorumunuzu yazın...
0/500