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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A fixed-window limiter tracks a start time and a count per client, resetting once the window elapses.
- 2ConcurrentHashMap's computeIfAbsent and merge keep bucket creation and rotation atomic under concurrent requests.
- 3Filters can short-circuit a request early by writing a response and returning without calling the chain.
Related explainers
go
package middleware import ( "net/http"
Per-IP write rate limiting in Gin
rate-limiting
middleware
concurrency
Intermediate
8 steps
python
import asyncio from dataclasses import dataclass import aiohttp
Bounded-concurrency HTTP fetching with asyncio
async
concurrency
semaphore
Intermediate
8 steps
rust
use std::sync::Arc; use axum::{ extract::{Multipart, State}, http::StatusCode,
Throttling file uploads in Axum with a Semaphore
concurrency
backpressure
multipart
Advanced
8 steps
java
public final class CaseConverter { private static final Pattern CAMEL_BOUNDARY = Pattern.compile("([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])");
Converting camelCase to snake_case in Java
regex
string-manipulation
utility-class
Intermediate
7 steps
java
@Component public class RegionCacheWarmer implements SmartInitializingSingleton { private static final Logger log = LoggerFactory.getLogger(RegionCacheWarmer.class);
Warming a Spring cache at startup
caching
startup-hook
dependency-injection
Intermediate
7 steps
go
package breaker import ( "errors"
How a circuit breaker works in Go
circuit-breaker
state-machine
concurrency
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-rate-limiting-filter-works-in-spring-explained-java-5abd/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.