Architecture patterns
Window counting limiters
Fixed windows, sliding logs and sliding counters, and the boundary burst that makes the simplest one leak.
A window counter answers one question: how many requests did this client make in the last period? It splits time into windows, such as one calendar minute, and counts requests in each. The three variants differ in how they handle the edges of a window. That edge behavior is the trade-off.
The big idea
Picture a bouncer who lets in 100 people per hour. He keeps a tally and resets it on the hour. The rule is easy to run, but the reset leaves a gap. Someone who lets 100 people in at 12:59 and 100 more at 1:00 has followed both hours' rules.
The analogy works for one bouncer. Across many servers, the tally itself must be shared, and that is a separate problem covered in the distributed limiter.
Walking through it
Fixed window
Keep one counter per client per window. Reject once the counter goes over the limit.
function fixedWindowAllow(counts: Map<string, number>, key: string, limit: number, nowMs: number, windowMs = 60_000) {
const k = `${key}:${Math.floor(nowMs / windowMs)}`
const n = (counts.get(k) ?? 0) + 1
counts.set(k, n) // in a real store, a TTL removes old windows
return n <= limit
}
const counts = new Map<string, number>()
let allowed = 0
for (let i = 0; i < 100; i++) allowed += fixedWindowAllow(counts, "client-1", 100, 59_000) ? 1 : 0 // 12:00:59
for (let i = 0; i < 100; i++) allowed += fixedWindowAllow(counts, "client-1", 100, 60_000) ? 1 : 0 // 12:01:00
console.log(allowed, "requests allowed in 2 seconds")Output:
200 requests allowed in 2 secondsBoth windows were within the limit, yet the service absorbed 200 requests in two seconds. That is the boundary burst: up to twice the intended rate across a window edge. For abuse limits this is often acceptable. For protecting a fragile backend, it is not.
Sliding window log
Store a timestamp for every accepted request. Count the timestamps in the last window. There is no boundary effect, because the window really slides. The cost is memory. A limit of 10,000 per hour across 1 million keys can mean up to 10 billion timestamps. In Redis this is usually a sorted set, which works for small limits such as login attempts.
Sliding window counter
Keep two counters, for the current window and the previous one. Estimate the sliding count by weighting the previous window by how much of it still overlaps the last minute:
Limit 100 per minute. It is 12:01:15, so 15 s into the current minute.
previous window (12:00) = 84 requests
current window (12:01) = 30 requests
overlap of previous = 1 − 15/60 = 0.75
estimate = 30 + 84 × 0.75 = 30 + 63 = 93 → allow (93 < 100)This assumes requests in the previous window were spread evenly. That is wrong for any one client, but the error is small in aggregate. Cloudflare reported that across 400 million requests, only about 0.003% were wrongly allowed or limited. Memory is two counters per key.
Common mistakes
- Trusting fixed windows at the edge. A client can double the limit by timing bursts around the boundary.
- Forgetting to expire old windows. Without a TTL, every window counter stays in memory forever.
- Using a sliding log for large limits. Memory grows with the limit, not with the number of active clients.
Check yourself
-
A fixed window allows 100 requests per minute. A client sends 100 requests at 12:00:58 and 100 more at 12:01:02. How many get through?
Answer: 200. The two bursts fall in different windows, and each is within its own limit.
-
The sliding counter multiplies the previous window by 0.75. What does 0.75 represent?
Answer: the share of the previous window that still falls inside the last 60 seconds.
Key takeaways
- A fixed window is one counter per window, cheap and simple, but it allows bursts of up to twice the limit at a boundary.
- A sliding log is exact, but its memory grows with the limit.
- A sliding counter estimates the sliding count from two counters. It is accurate in aggregate and cheap.
- Always set a TTL on window state.
Further learning
Next, see how a bucket with a fixed capacity and refill rate allows bursts without boundary effects in the token bucket.
Related
Rate limiting
Why systems cap how much work a client can send, which algorithm counts, where the limit lives and what the client is told. Start here for the rate-limiting map.
Token bucket
A bucket that holds tokens and refills over time, giving you two dials for a sustained rate and a burst size. The most common shape for API limits.
Distributed limiter on Redis
How many servers share one rate-limit count without races, using an atomic Redis Lua script, one clock, expiring keys and a deliberate failure mode.