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.

One client sends requests to three limiters that each ask Redis. Fixed window counter: one string key per window, rl:alice:0 = 3, with INCR and EXPIRE. Sliding window log: one sorted set rl:alice with a member per allowed request, scored by its time. Sliding window counter: two keys, the previous and the current window, combined as prev times weight plus curr
Three ways to remember "how many in the last 10 s": one counter, one timestamp per request, or two counters and a weight.

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).

A time axis from 0 to 20 seconds with a window boundary at 10. Four requests at second 9 and four at second 10. The fixed window counter allows all eight, because they fall in windows 0 and 1. The sliding window log allows the four at second 9 and rejects the four at second 10 with 429
Eight requests a second apart: two fixed windows each see only 4, the sliding log sees 8 within 10 s and stops the second burst.

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:

estimate=prev×10−elapsed10+curr

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.

Window 0 from 0 to 10 seconds holds 4 requests, window 1 from 10 to 20 holds 1. At t = 15 the last 10 seconds cover the second half of window 0 and the first half of window 1. The estimate is 4 times 5 over 10 plus 1, which is 3, below the limit of 4, so the request is allowed
At t = 15 half of the previous window is still inside the last 10 s, so its 4 count as 2.

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 windowsliding logsliding counter
Redis typestring (integer)sorted settwo strings
commandsINCR, EXPIREZREMRANGEBYSCORE, ZCARD, ZADDGET ×2, INCR
memory per client1 counter1 member per request in the window2 counters
most in any 10 sup to 2 × limitexactly the limitabout the limit
Demo 2 (boundary)8 allowed4 allowed4 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.