java 51 lines · 8 steps

How a rate-limiting filter works in Spring

A per-client fixed-window rate limiter built as a Spring servlet filter with concurrent-safe counters.

Explained by highlit
1@Component
2public class RateLimitingFilter extends OncePerRequestFilter {
3 
4 private static final int MAX_REQUESTS = 100;
5 private static final Duration WINDOW = Duration.ofMinutes(1);
6 
7 private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
8 
9 @Override
10 protected void doFilterInternal(HttpServletRequest request,
11 HttpServletResponse response,
12 FilterChain filterChain)
13 throws ServletException, IOException {
14 
15 String clientKey = resolveClientKey(request);
16 Bucket bucket = buckets.computeIfAbsent(clientKey,
17 key -> new Bucket(Instant.now(), new AtomicInteger()));
18 
19 if (bucket.isExpired()) {
20 bucket = buckets.merge(clientKey,
21 new Bucket(Instant.now(), new AtomicInteger()),
22 (existing, fresh) -> existing.isExpired() ? fresh : existing);
23 }
24 
25 int used = bucket.count().incrementAndGet();
26 long remaining = Math.max(0, MAX_REQUESTS - used);
27 response.setHeader("X-RateLimit-Limit", String.valueOf(MAX_REQUESTS));
28 response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
29 
30 if (used > MAX_REQUESTS) {
31 response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
32 response.setHeader("Retry-After", String.valueOf(WINDOW.getSeconds()));
33 response.setContentType(MediaType.APPLICATION_JSON_VALUE);
34 response.getWriter().write("{\"error\":\"rate limit exceeded\"}");
35 return;
36 }
37 
38 filterChain.doFilter(request, response);
39 }
40 
41 private String resolveClientKey(HttpServletRequest request) {
42 String apiKey = request.getHeader("X-Api-Key");
43 return apiKey != null ? apiKey : request.getRemoteAddr();
44 }
45 
46 private record Bucket(Instant start, AtomicInteger count) {
47 boolean isExpired() {
48 return Instant.now().isAfter(start.plus(WINDOW));
49 }
50 }
51}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A fixed-window limiter tracks a start time and a count per client, resetting once the window elapses.
  2. 2ConcurrentHashMap's computeIfAbsent and merge keep bucket creation and rotation atomic under concurrent requests.
  3. 3Filters can short-circuit a request early by writing a response and returning without calling the chain.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How a rate-limiting filter works in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code