Implement a Sliding-Window Rate Limit
Use a sliding window or token bucket with shared state so limits are accurate across instances and fair at the boundaries of fixed windows.
Task
Add a rate limiter that is accurate across multiple servers and avoids the burst at fixed-window boundaries.
When to use this
A per-instance fixed-window limiter lets a caller send double the allowed rate at the window boundary, and it multiplies the limit by the number of replicas.
Prerequisites
- A shared store for the counters
- A key per client identity
Steps
1. Choose sliding-window or token-bucket instead of a fixed window.
Expected output: The limit is smooth rather than bursty at boundaries.
2. Store counters or tokens in a shared store keyed by client and route.
Expected output: The limit applies globally, not per instance.
3. Update and read counters atomically with a Lua script or an atomic operation.
Expected output: Concurrent requests cannot exceed the limit.
4. Return 429 with Retry-After and limit headers so clients back off correctly.
redis-cli --eval ratelimit.lua client:42 , 100 60Expected output: Well-behaved clients adjust automatically.
Troubleshooting
Problem
Counters lost on restart
Solution
Keep the counter in a shared durable store, not in process memory.
Problem
Limit applied to the wrong key
Solution
Key by the correct identity (api key or user), not by the ephemeral connection address.
Limitations
- A shared-store limiter adds a round trip per request and can itself become a bottleneck.
- Distributed rate limiting is approximate under partitions and needs a documented failure mode.
Context
Add a rate limiter that is accurate across multiple servers and avoids the burst at fixed-window boundaries. A per-instance fixed-window limiter lets a caller send double the allowed rate at the window boundary, and it multiplies the limit by the number of replicas.
Steps
- Choose sliding-window or token-bucket instead of a fixed window. Expected: The limit is smooth rather than bursty at boundaries.
- Store counters or tokens in a shared store keyed by client and route. Expected: The limit applies globally, not per instance.
- Update and read counters atomically with a Lua script or an atomic operation. Expected: Concurrent requests cannot exceed the limit.
- Return 429 with Retry-After and limit headers so clients back off correctly. Expected: Well-behaved clients adjust automatically.
const allowed = await redis.eval(TOKEN_BUCKET_LUA, 1, `rl:${clientId}`, now, rate, capacity);
if (!allowed) {
res.setHeader('Retry-After', '1');
return res.status(429).json({ error: { code: 'rate_limited' } });
}
Verify
- The limit is smooth rather than bursty at boundaries.
- The limit applies globally, not per instance.
- Concurrent requests cannot exceed the limit.
- Well-behaved clients adjust automatically.
Limits
- A shared-store limiter adds a round trip per request and can itself become a bottleneck.
- Distributed rate limiting is approximate under partitions and needs a documented failure mode.