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
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 steps
java
public class TimedSocketReader { private static final int READ_TIMEOUT_MS = 5_000; private static final int CONNECT_TIMEOUT_MS = 3_000;
Reading a socket with connect and read timeouts
sockets
timeouts
io
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.