Back to blog
Coding

API Rate Limiting Patterns in 2026: Choosing the Right Algorithm

5 min read

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

If you're unsure which algorithm to start with, the 2026 default recommendation is a sliding window counter (not the full log variant) — it handles most API scenarios with low memory, near-exact accuracy, and no boundary bursts (Redis rate limiting guide).

Choosing an algorithm: three factors

Pick based on three factors, weighed against each other rather than in isolation (DigitalApplied):

  1. 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.
  2. 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.
  3. 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.

Keep reading

Discussion