The idea: count requests per window
A rate limit such as "4 requests per 10 seconds" needs a counter per client that every API server can see, so it usually lives in Redis. The limiter asks Redis before each request and answers 429 Too Many Requests when the client is over. The question is what exactly to count, and what to store. This page runs the same requests from one client, alice, through three window-based answers at once.
Press Send to make alice send 1, 2 or 4 requests at the current second, and Wait to move the clock. Each lane shows its Redis keys, the commands it ran for the last request, and the answer. The timeline under the lanes keeps a dot per request (green allowed, red 429), and its shading shows the window each limiter counts right now. The column on the right of the timeline counts the most requests each limiter let through in any 10 seconds, the number a backend actually feels.
Fixed window counter
Cut time into windows of 10 s: window w = floor(t / 10). Each window has its own key, and each request increments it:
count = INCR rl:alice:{w}
if count == 1: EXPIRE rl:alice:{w} 10
if count > 4: return 429
That is one small integer per client, one round trip, and old keys clean themselves up. The counter keeps going up on rejected requests (that is what INCR does), which is harmless. Run INCR and EXPIRE together, in MULTI/EXEC or a Lua script; otherwise a crash between them leaves a key that never expires.
The flaw is the boundary. The counter forgets everything at the start of a window. A client that sends 4 at t = 9 and 4 at t = 10 gets all 8 through in about a second, twice the limit (Demo 2: burst at the window boundary).
Sliding window log
Keep the time of every allowed request in a sorted set, scored by its time. For a request at t:
ZREMRANGEBYSCORE rl:alice -inf (t - 10) # drop what left the window n = ZCARD rl:alice if n >= 4: return 429 ZADD rl:alice t <request id> EXPIRE rl:alice 10
The window now really slides: it is always the last 10 seconds, (t − 10, t], so no 10 s span ever holds more than 4 (the log's "max in 10 s" never passes 4 in any demo). The price is memory: one member per allowed request, so a limit of 10,000 per hour means up to 10,000 members per client. Old members are not removed when they expire, only by the next request's ZREMRANGEBYSCORE; the page draws them grey until then. The four commands must run as one Lua script, or two servers can both read ZCARD = 3 and both add. Some versions also add rejected requests to the log, which punishes a client that keeps retrying; this page adds only allowed ones. How Redis keeps a sorted set (listpack, then skiplist) is in How a Redis Sorted Set Is Stored.
Sliding window counter
The sliding window counter keeps the fixed window's two latest counters and blends them. If the current window started elapsed seconds ago, the last 10 seconds hold all of the current window and the last (10 − elapsed)/10 of the previous one. Assuming the previous window's requests were spread evenly:
The request is allowed when the estimate is below 4, and then INCR the current key (with EXPIRE of two windows, so the previous key is still there). Two integers per client, like the fixed window, but with no cliff at the boundary.
The even-spread guess can be wrong both ways. In Demo 3 the previous window's 4 came at t = 9, so at t = 15 they still all fall inside the last 10 s, yet count only 2: the counter allows 2 more, and 6 get through in 10 s. In Demo 4 they came at t = 1, long gone at t = 13, yet still count 2.8: the counter rejects 2 requests the exact log allows. With real traffic from many requests the error is small; Cloudflare measured about 0.003% of requests decided wrongly.
The three compared
| fixed window | sliding log | sliding counter | |
|---|---|---|---|
| Redis type | string (integer) | sorted set | two strings |
| commands | INCR, EXPIRE | ZREMRANGEBYSCORE, ZCARD, ZADD | GET ×2, INCR |
| memory per client | 1 counter | 1 member per request in the window | 2 counters |
| most in any 10 s | up to 2 × limit | exactly the limit | about the limit |
| Demo 2 (boundary) | 8 allowed | 4 allowed | 4 allowed |
Under steady overload (Demo 5: 2 requests every 2 s) all three settle near 4 per window; they differ only in which requests near a boundary get through.
Compared with the token bucket
A token bucket doesn't count windows at all: it stores a token count and the time of the last refill, adds tokens at a fixed rate, and lets a request through if it can take one. Its burst size is a parameter of its own (the bucket's capacity), separate from the long-run rate, and like the sliding counter it needs only two numbers per client. Window limits are easier to explain to users ("100 per minute") and map directly to RateLimit-Limit style headers. The token bucket and the leaky bucket are animated in Rate Limiting: Token Bucket vs Leaky Bucket. Where such a limiter usually runs, per consumer in front of the services, is shown in How an API Gateway Handles a Request; how Redis runs a Lua script atomically is in Redis Transactions and Scripts.
What the page leaves out
Real time in milliseconds instead of whole seconds; many clients, API keys and routes, each with its own keys; the Retry-After and RateLimit-* response headers; Redis Cluster key placement (hash tags so a client's keys sit on one shard); clock skew between API servers (scripts often use Redis's own TIME); and local in-memory limiters that sync with Redis only now and then to save round trips.