Rate limiting looks simple until you have to run it correctly across multiple servers under real traffic. The algorithm you pick determines whether your API gracefully absorbs bursts or produces confusing edge-case rejections, and the infrastructure you run it on determines whether the rate limiter itself becomes your next outage.
The four core algorithms
2026 engineering references converge on the same four options, each suited to a different traffic shape (DigitalApplied):
- Fixed Window — count requests in discrete time buckets (e.g., per-minute). Simplest to implement and reason about, but has a boundary problem: a client can send a full window's worth of requests at 0:59 and another full window's worth at 1:00, doubling the effective rate right at the boundary.
- Sliding Window — smooths traffic using a rolling time window instead of discrete buckets, avoiding the fixed window's edge-spike problem, at the cost of somewhat more implementation complexity (Arcjet).
- Token Bucket — represents capacity as tokens that accumulate over time up to a configurable maximum; a client consuming tokens faster than the refill rate gets rejected, while an idle client builds up burst credit. This suits APIs that need to tolerate short bursts while still enforcing an average throughput ceiling (DigitalApplied).
- Leaky Bucket — processes requests at a constant outflow rate regardless of input burstiness, effectively queue-based; good for smoothing load onto a downstream system with fixed processing capacity.
For strict compliance-sensitive contexts — payment APIs, healthcare data — sliding window log (the more precise, higher-memory variant of sliding window that tracks individual request timestamps rather than a counter) provides the most accurate counting with no boundary exploits (DigitalApplied).
Note
Choosing an algorithm: three factors
Pick based on three factors, weighed against each other rather than in isolation (DigitalApplied):
- Traffic pattern. Bursty APIs (batch imports, webhook replay, client-driven retries) favor token bucket, which explicitly tolerates bursts as long as average throughput stays in bounds.
- Resource cost. Sliding window log is the most accurate but stores a timestamp per request per client — memory cost scales with request volume, not just client count. Sliding window counter approximates this with far less memory.
- Team complexity budget. Fixed window is trivial to implement and debug; sliding window log is the most complex to implement correctly, particularly across distributed nodes.
The distributed problem: Redis becomes critical infrastructure
Rate limiting logic is easy on a single server — an in-memory counter works fine. It gets hard the moment you have more than one API server, because now every server needs to agree on a client's current usage. The standard solution is a centralized store (almost always Redis), but that store then becomes critical infrastructure: high availability, replication, and low-latency network communication all become essential, since every request now depends on reaching the shared counter (OneUptime / ADHDecode).
Two failure modes specifically called out in 2026 guidance:
- Don't rely on exact timestamp synchronization across machines — clock drift between API servers can silently corrupt sliding window calculations if the algorithm assumes tightly synchronized clocks (OneUptime).
- Don't assume Redis is always available — build graceful degradation into the rate limiter itself, so a Redis outage doesn't either take down your entire API (fail-closed everywhere) or silently disable rate limiting entirely (fail-open everywhere) without you knowing (OneUptime).
Correctness under concurrency also matters: use atomic Lua scripts for the check-and-increment operation, rather than a separate GET then SET, which race under concurrent requests from the same client hitting different API servers simultaneously (OneUptime).
A production-grade Lua-scripted token bucket
-- KEYS[1] = bucket key, ARGV[1] = max_tokens, ARGV[2] = refill_rate,
-- ARGV[3] = now (unix ms), ARGV[4] = requested_tokens
local bucket = redis.call("HMGET", KEYS[1], "tokens", "last_refill")
local tokens = tonumber(bucket[1]) or tonumber(ARGV[1])
local last_refill = tonumber(bucket[2]) or tonumber(ARGV[3])
local elapsed = (tonumber(ARGV[3]) - last_refill) / 1000
local refill = elapsed * tonumber(ARGV[2])
tokens = math.min(tonumber(ARGV[1]), tokens + refill)
if tokens >= tonumber(ARGV[4]) then
tokens = tokens - tonumber(ARGV[4])
redis.call("HMSET", KEYS[1], "tokens", tokens, "last_refill", ARGV[3])
redis.call("EXPIRE", KEYS[1], 3600)
return 1 -- allowed
else
return 0 -- rejected
end
Running this as a single atomic Lua script avoids the race condition that a separate read-then-write in application code would introduce under concurrent requests.
Comparison table
| Algorithm | Burst tolerance | Memory cost | Accuracy | Implementation complexity |
|---|---|---|---|---|
| Fixed Window | Poor (boundary doubling) | Lowest | Low at boundaries | Trivial |
| Sliding Window Counter | Moderate | Low | High, near-exact | Moderate |
| Sliding Window Log | Moderate | Highest (per-request timestamps) | Exact | High |
| Token Bucket | High (by design) | Low | High for average rate | Moderate |
| Leaky Bucket | None (constant outflow) | Low | High for downstream protection | Moderate |
Performance budget
If your rate limiting layer adds more than 5-10ms of latency per request or consumes significant CPU, it's time to optimize (OneUptime). A common optimization: cache positive ("allowed") results locally for a short duration, but always re-check Redis when the local cache says "blocked" — and keep the cache TTL short so the local view doesn't drift far from the distributed counters (OneUptime).
Actionable takeaway
Default to a sliding window counter unless you have a specific reason not to — it's the best general-purpose choice for accuracy versus memory cost in a distributed Redis setup. Switch to token bucket specifically for APIs that need to tolerate legitimate bursts (webhook delivery, batch operations). Reserve sliding window log for compliance-sensitive endpoints where exact counting matters more than memory efficiency. Whatever algorithm you choose, implement the check-and-increment as an atomic Lua script, not a separate read/write, and explicitly decide — don't accidentally default into — whether your system fails open or closed when Redis is unreachable.
Sources: DigitalApplied — API Rate Limiting Strategies: 2026 Engineering Reference, Arcjet — Rate-limiting algorithms compared, ADHDecode — Distributed Rate Limiting with Redis, OneUptime — How to Build a Distributed Rate Limiter with Redis, OneUptime — How to Fix API Rate Limiting Performance
Get new posts as they publish
No spam — just the next post, straight to your inbox.