Fixed Window Counter: Simplicity With a Known Edge Case
The failure mode it solves
Sometimes the requirement is not perfect fairness. It is speed and simplicity. Fixed Window Counter is the fastest algorithm in this project and the easiest to reason about: count requests in a window, then reset. It is a good default for internal services or low-risk endpoints where boundary spikes are acceptable.
How the algorithm works in this service
Each IP + endpoint pair maps to a Redis key. The Lua script increments the counter and sets the TTL on first write. That TTL defines the window boundary.
Step-by-step flow:
INCRthe Redis keyrate-limit:{ip}:{endpoint}.- If the counter is
1, set the TTL towindowSizeseconds. - If
count <= maxRequests, allow. Otherwise reject.
Diagram
The window is fixed to TTL boundaries, not relative to each client. That makes the algorithm fast, but it also creates boundary spikes.
Worked example: the boundary spike
Window size is 60 seconds with maxRequests 100. A client sends 100 requests between 00:59 and 01:00. All 100 are accepted. At 01:00 the counter resets, and the client can immediately send another 100 requests. The effective rate is 200 requests in two seconds, even though the limit is 100 per minute. This is the known tradeoff.
Where it fits versus the other algorithms
| Algorithm | Strength | Weakness | Best fit |
|---|---|---|---|
| Fixed Counter | Fastest, smallest memory footprint | Boundary spikes | Internal or low-risk APIs |
| Token Bucket | Burst-friendly with average control | Not uniform spacing | Public APIs |
| Leaky Bucket | Smooths output rate | Adds queueing latency | Backend protection |
| Sliding Window | Most accurate enforcement | Highest Redis cost | Billing and security |
Configuration in this project
rate-limit:
fallback:
algorithm: FIXED_COUNTER
maxRequests: 1000
windowSize: 60windowSize is in seconds. maxRequests is the limit within that window. Increasing windowSize lowers per-second allowance but makes the boundary spike larger in absolute terms. If boundary spikes are unacceptable, use Sliding Window instead.
Redis state and TTL
Fixed Counter uses a single Redis string with a TTL:
key: rate-limit:{ip}:{endpoint}
value: request_count
ttl: windowSizeTTL defines the window boundary. Keep it aligned with the intended limits so clients can reason about reset timing from X-RateLimit-ResetOn.