Anıl Şenocak

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.
Modern applications face several challenges that make rate limiting necessary:
Traditional in-memory rate limiters work well for single-instance applications but fall short in distributed environments where multiple service instances handle requests independently.
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
)
}
}
}
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")
}
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.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:"
The library uses Spring Boot’s auto-configuration mechanism to automatically set up the rate limiter components:
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 userRate 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