Rate Limiting in Spring Boot REST API
Fast recall
Quick revision
Core answer
Ability to implement rate limiting at the application level
Using a HashMap for rate limiting state — not thread-safe and not distributed
Mention the different algorithms (token bucket, sliding window, fixed window) and their trade-offs
Interview-ready guide
Interview answer
Rate limiting protects your API from being overwhelmed by a single client — whether through abuse, misconfigured retry logic, or denial-of-service. The implementation strategy matters significantly for distributed deployments.
Why in-memory rate limiting fails in production
- Each application instance maintains its own counter
- A client making 100 requests per minute across 5 instances can make 500 total without triggering any single instance's limit
- In-memory works only for single-instance deployments or as a local pre-check before a distributed check
Algorithms
- Fixed window: count requests per time window (e.g., per minute); simple but allows 2× the limit at window boundaries (end of old window + start of new)
- Sliding window: track individual request timestamps; accurate but memory-intensive
- Token bucket: a bucket of tokens refills at a fixed rate; clients consume tokens; allows controlled bursting up to the bucket capacity
Redis-based implementation
- Use an atomic Lua script or Redis
ZADD/ZREMRANGEBYSCOREon a sorted set keyed byclientId:minute - Bucket4j and Resilience4j both support Redis-backed rate limiting with Spring Boot integration
Response
- Return
429 Too Many RequestswithRetry-After: 60header - Rate limit key should be authenticated user ID or API key — never just IP address behind shared NAT
Rate Limiting in Spring Boot REST API
Direct Answer
Implement rate limiting with Bucket4j (in-memory or Redis-backed) using Spring AOP or a servlet filter. For distributed systems, use Redis with Lua script for atomic counter operations. Return 429 with retry-After header on limit exceeded.
Code Example
Rate limiting protects an API from abuse and noisy tenants. The token-bucket algorithm (Bucket4j) with Redis as the store works across multiple app instances.
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-redis</artifactId>
<version>8.10.0</version>
</dependency>
@Configuration
public class RateLimitConfig {
@Bean
public ProxyManager<String> proxyManager(LettuceConnectionFactory cf) {
RedisClient client = RedisClient.create("redis://localhost:6379");
return Bucket4jLettuce.casBasedBuilder(client).build();
}
}
@Component
@RequiredArgsConstructor
public class RateLimitingFilter extends OncePerRequestFilter {
private final ProxyManager<String> buckets;
private Bucket resolveBucket(String key, int capacity, Duration window) {
BucketConfiguration cfg = BucketConfiguration.builder()
.addLimit(Bandwidth.classic(capacity,
Refill.intervally(capacity, window)))
.build();
return buckets.builder().build(key, () -> cfg);
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
FilterChain chain) throws IOException, ServletException {
String apiKey = Optional.ofNullable(req.getHeader("X-API-Key"))
.orElse(req.getRemoteAddr());
Bucket bucket = resolveBucket("rl:" + apiKey, 100, Duration.ofMinutes(1));
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
resp.setHeader("X-RateLimit-Limit", "100");
resp.setHeader("X-RateLimit-Remaining", String.valueOf(probe.getRemainingTokens()));
if (probe.isConsumed()) {
chain.doFilter(req, resp);
} else {
long retryAfter = probe.getNanosToWaitForRefill() / 1_000_000_000L;
resp.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
resp.setHeader("Retry-After", String.valueOf(retryAfter));
resp.getWriter().write("{\"error\":\"Too many requests\"}");
}
}
}
// Per-endpoint limits via annotation + AOP (sketch)
@RateLimit(capacity = 10, window = "1m")
@PostMapping("/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) { ... }
- Token bucket is the usual pick: burst up to
capacity, refillNtokens per window. Simple, fair, and easy to communicate to API consumers. - Use Redis (or a similar shared store) — in-memory limits in each pod under-count a user making requests to multiple instances behind a load balancer.
- Key the bucket by the strongest identity you have, in priority order: authenticated user → API key → IP. Never rely on IP alone (NAT and proxies share addresses).
- Always emit
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset, and on429setRetry-After— clients then back off politely instead of hammering. - For gateway-level limits, Spring Cloud Gateway's
RequestRateLimiterfilter (also backed by Redis) avoids adding a filter per service.
Common Mistakes & Pitfalls
Using a HashMap for rate limiting state — not thread-safe and not distributed
Check your understanding
Answer these without looking back at the article.
- What is the difference between token bucket and sliding window rate limiting?
- How do you rate limit by user tier (free vs premium)?
- Should rate limiting be in the application or at the API gateway?
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.