Rate Limiting
Why Rate Limit
A bot that hits POST /api/orders 500 times a second, or a buggy app stuck in a loop, can take down order-service for everyone. The gateway can limit each client (by user ID, API key or IP). Extra requests get 429 Too Many Requests without touching the service.
Spring Cloud Gateway uses the token bucket algorithm: each key has a bucket holding up to burstCapacity tokens, refilled at replenishRate tokens per second. A request takes one token; an empty bucket means 429.
Token Bucket in Plain Java
Example
A bucket with capacity 5 and refill 2 tokens/second, with a simulated clock in milliseconds:
public class Main {
public static void main(String[] args) {
TokenBucket bucket = new TokenBucket(5, 2.0); // burst 5, refill 2/s
long[] requestTimesMs = {0, 0, 0, 0, 0, 0, 0, 1000, 1000, 1000, 2500, 2500, 2500, 2500};
int n = 1;
for (long t : requestTimesMs) {
boolean allowed = bucket.tryConsume(t);
System.out.printf("t=%4dms request %2d -> %-22s (tokens left: %d)%n",
t, n++, allowed ? "200 OK" : "429 Too Many Requests", (int) bucket.tokens());
}
}
}
class TokenBucket {
private final int capacity;
private final double refillPerSecond;
private double tokens;
private long lastRefillMs = 0;
TokenBucket(int capacity, double refillPerSecond) {
this.capacity = capacity;
this.refillPerSecond = refillPerSecond;
this.tokens = capacity; // starts full
}
boolean tryConsume(long nowMs) {
double elapsedSeconds = (nowMs - lastRefillMs) / 1000.0;
tokens = Math.min(capacity, tokens + elapsedSeconds * refillPerSecond);
lastRefillMs = nowMs;
if (tokens >= 1) {
tokens -= 1;
return true;
}
return false;
}
double tokens() { return tokens; }
}Output
t= 0ms request 1 -> 200 OK (tokens left: 4) t= 0ms request 2 -> 200 OK (tokens left: 3) t= 0ms request 3 -> 200 OK (tokens left: 2) t= 0ms request 4 -> 200 OK (tokens left: 1) t= 0ms request 5 -> 200 OK (tokens left: 0) t= 0ms request 6 -> 429 Too Many Requests (tokens left: 0) t= 0ms request 7 -> 429 Too Many Requests (tokens left: 0) t=1000ms request 8 -> 200 OK (tokens left: 1) t=1000ms request 9 -> 200 OK (tokens left: 0) t=1000ms request 10 -> 429 Too Many Requests (tokens left: 0) t=2500ms request 11 -> 200 OK (tokens left: 2) t=2500ms request 12 -> 200 OK (tokens left: 1) t=2500ms request 13 -> 200 OK (tokens left: 0) t=2500ms request 14 -> 429 Too Many Requests (tokens left: 0)
A burst of 5 passes at once, then the client can only do 2 requests per second on average. That is exactly what burstCapacity: 5 and replenishRate: 2 mean.
RequestRateLimiter with Redis
Every gateway instance must share the same buckets, so Spring stores them in Redis. Add spring-boot-starter-data-redis-reactive, then:
spring:
data:
redis:
host: localhost
port: 6379
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 2
redis-rate-limiter.burstCapacity: 5
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"@Configuration
class RateLimitConfig {
/** One bucket per logged-in user; fall back to the client IP. */
@Bean
KeyResolver userKeyResolver() {
return exchange -> exchange.getPrincipal()
.map(Principal::getName)
.switchIfEmpty(Mono.fromSupplier(() ->
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()));
}
}Example
The 6th request in the same second is rejected:
for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w "%{http_code} " -X POST localhost:8080/api/orders -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @order.json; doneExample output
201 201 201 201 201 429 HTTP/1.1 429 Too Many Requests X-RateLimit-Remaining: 0 X-RateLimit-Burst-Capacity: 5 X-RateLimit-Replenish-Rate: 2 X-RateLimit-Requested-Tokens: 1
Exercise
Exercise: Change the TokenBucket example so expensive requests (e.g. POST /api/orders) cost 3 tokens and cheap ones cost 1. Add a tryConsume(long nowMs, int cost) method.
Show answer
boolean tryConsume(long nowMs, int cost) {
double elapsedSeconds = (nowMs - lastRefillMs) / 1000.0;
tokens = Math.min(capacity, tokens + elapsedSeconds * refillPerSecond);
lastRefillMs = nowMs;
if (tokens >= cost) {
tokens -= cost;
return true;
}
return false;
}
// In the gateway this is redis-rate-limiter.requestedTokens: 3 on the order route.