Implementing API Rate Limiting

1. Using Bucket4j for Rate Limiting

Example: Bucket4j dependencies

<dependency>
  <groupId>com.bucket4j</groupId>
  <artifactId>bucket4j_jdk17-core</artifactId>
  <version>8.10.1</version>
</dependency>

2. Configuring Rate Limits

Example: Token bucket with 100 req/min bandwidth

Bucket bucket = Bucket.builder()
  .addLimit(Bandwidth.builder().capacity(100).refillIntervally(100, Duration.ofMinutes(1)).build())
  .build();

3. Implementing Filter-Based Rate Limiting

Example: Per-IP rate limit filter

@Component
public class RateLimitFilter extends OncePerRequestFilter {
  private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
  @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    Bucket b = buckets.computeIfAbsent(key(req), k -> newBucket());
    ConsumptionProbe probe = b.tryConsumeAndReturnRemaining(1);
    if (probe.isConsumed()) {
      res.addHeader("X-RateLimit-Remaining", String.valueOf(probe.getRemainingTokens()));
      chain.doFilter(req, res);
    } else {
      res.setStatus(429);
      res.addHeader("Retry-After", String.valueOf(probe.getNanosToWaitForRefill() / 1_000_000_000));
    }
  }
  private String key(HttpServletRequest r) { return r.getRemoteAddr(); }
  private Bucket newBucket() { /* see 49.2 */ return null; }
}

4. Using Redis for Distributed Rate Limiting

Example: Distributed rate limit dependencies

<dependency>
  <groupId>com.bucket4j</groupId>
  <artifactId>bucket4j_jdk17-redis-common</artifactId>
</dependency>
<dependency>
  <groupId>com.bucket4j</groupId>
  <artifactId>bucket4j_jdk17-lettuce</artifactId>
</dependency>
Note: A distributed proxy ensures all app instances share the same token bucket — required behind load balancers.

5. Implementing User-Based Rate Limits

Example: Rate limit key: authenticated user or IP

private String key(HttpServletRequest r) {
  Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  return (auth != null && auth.isAuthenticated()) ? "user:" + auth.getName() : "ip:" + r.getRemoteAddr();
}

6. Implementing Endpoint-Specific Rate Limits

Example: Per-endpoint bandwidth limits

private static final Map<String, Bandwidth> LIMITS = Map.of(
  "/api/auth/login",  Bandwidth.simple(5,  Duration.ofMinutes(1)),
  "/api/search",      Bandwidth.simple(60, Duration.ofMinutes(1)),
  "/api/orders",      Bandwidth.simple(120,Duration.ofMinutes(1))
);

7. Returning Rate Limit Headers (X-RateLimit-*)

Header Meaning
X-RateLimit-Limit Max requests per window
X-RateLimit-Remaining Tokens left
X-RateLimit-Reset Epoch seconds when refilled
Retry-After Seconds to wait (on 429)

8. Handling Rate Limit Exceeded (429 Too Many Requests)

Example: Respond with 429 Too Many Requests

res.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
res.setContentType("application/problem+json");
res.getWriter().write("""
  {"type":"https://example.com/rate-limit","title":"Too Many Requests","status":429}
  """);

9. Implementing Sliding Window Algorithm

Algorithm Pros / Cons
Fixed window Simple; burst at edges
Sliding log Precise; high memory
Sliding window counter Approximate; low memory (good default)
Token bucket Allows bursts up to capacity
Leaky bucket Smooth output rate

10. Testing Rate Limiting Logic

Example: Test rate limit exceeded

@Test void exceedsLimit() throws Exception {
  for (int i = 0; i < 100; i++)
    mvc.perform(get("/api/data")).andExpect(status().isOk());
  mvc.perform(get("/api/data"))
     .andExpect(status().isTooManyRequests())
     .andExpect(header().exists("Retry-After"));
}