Spring Boot cookbook · Intermediate
Rate limit a Spring Boot API with Redis
Limit each API client to N requests per minute across all instances and return 429 with Retry-After.
Approach
Implement a OncePerRequestFilter (or HandlerInterceptor) that increments a Redis counter per client and window with a Lua script (INCR + EXPIRE atomically). Identify clients by authenticated principal or API key, falling back to IP. Return a 429 Problem Detail with Retry-After when exceeded.
Solution
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private static final RedisScript<List> SCRIPT = RedisScript.of("""
local c = redis.call('INCR', KEYS[1])
if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return {c, redis.call('TTL', KEYS[1])}
""", List.class);
private final StringRedisTemplate redis;
private final int limit = 100;
private final int windowSeconds = 60;
public RateLimitFilter(StringRedisTemplate redis) { this.redis = redis; }
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String client = Optional.ofNullable(req.getUserPrincipal()).map(Principal::getName)
.orElse("ip:" + req.getRemoteAddr());
long window = Instant.now().getEpochSecond() / windowSeconds;
List<?> result = redis.execute(SCRIPT, List.of("rl:" + client + ":" + window), String.valueOf(windowSeconds));
long count = ((Number) result.get(0)).longValue();
long ttl = ((Number) result.get(1)).longValue();
res.setHeader("RateLimit-Limit", String.valueOf(limit));
res.setHeader("RateLimit-Remaining", String.valueOf(Math.max(0, limit - count)));
if (count > limit) {
res.setStatus(429);
res.setHeader("Retry-After", String.valueOf(Math.max(1, ttl)));
res.setContentType("application/problem+json");
res.getWriter().write("{\"title\":\"Too Many Requests\",\"status\":429}");
return;
}
chain.doFilter(req, res);
}
}
Pitfalls
- In-memory buckets per instance (limits multiply with replicas).
getRemoteAddr()behind a proxy withoutserver.forward-headers-strategy=native.- Filter order: run after authentication if limiting per user.
Variations
- Bucket4j with a Redis/JCache backend for token-bucket semantics.
- Spring Cloud Gateway's
RequestRateLimiterfilter at the edge.
Further reading: Spring Data Redis: Scripting
Reference: https://docs.spring.io/spring-data/redis/reference/redis/scripting.html
More Spring Boot recipes
- Add correlation IDs to Spring Boot logs
- Add retries and a circuit breaker with Resilience4j
- Authenticate machine clients with API keys in Spring Security
- Cache service results in Redis with @Cacheable
- Consume Kafka messages with retries and a dead-letter topic
- Handle concurrent updates with optimistic locking
- Implement soft delete with Hibernate
- Make POST endpoints idempotent in Spring Boot