Rate Limiting Algorithm & Throttling Playground
Compare how Token Bucket, Fixed Window, Sliding Window Log, and Leaky Bucket handle sudden traffic bursts. Discover why Fixed Window permits $2\times$ boundary spikes and how Token Bucket accommodates temporary bursts without starvation.
Token Bucket has 10 initial tokens. 10 requests consume all tokens immediately and succeed. The remaining 6 requests are rejected with HTTP 429 until the bucket refills at 10 tokens/sec.
Real-World Case Study: Designing a Distributed Rate Limiter
In a single server, rate limiting is straightforward: maintain atomic counters in memory.
In a modern cloud infrastructure with 200 API gateway instances distributed across 3 global regions serving 500,000 requests per second, in-memory counters fail:
- A malicious actor can cycle through different API gateway pods, multiplying their allowed quota by 200x!
- You must design a Distributed Rate Limiter that enforces precise rate limits globally with sub-millisecond overhead.
1. The Distributed Race Condition
Suppose you use an external shared cache (Redis) with naive get-and-set commands:
# THE DISTRIBUTED RACE HAZARD
current_count = redis.get(client_id)
if current_count < 100:
redis.incr(client_id)
return ALLOW
return REJECT
If two concurrent requests arrive at Gateway Pod 1 and Gateway Pod 2 simultaneously:
- Both pods execute
redis.get(client_id)and receive99. - Both pods conclude
99 < 100. - Both pods increment and allow the request.
- Quota was exceeded! Under heavy concurrency, race conditions allow thousands of requests past the rate limit.
2. The Solution: Atomic Redis Lua Scripts
Redis processes Lua scripts atomically in a single-threaded execution context. No other command or script can run while a Lua script executes:
-- ATOMIC SLIDING WINDOW COUNTER IN REDIS LUA
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current_time = tonumber(ARGV[2])
local window = tonumber(ARGV[3])
local clear_before = current_time - window
-- 1. Remove expired timestamps
redis.call('ZREMRANGEBYSCORE', key, 0, clear_before)
-- 2. Count current elements in window
local current_requests = redis.call('ZCARD', key)
if current_requests < limit then
-- 3. Add current timestamp with unique member
redis.call('ZADD', key, current_time, current_time)
redis.call('EXPIRE', key, window)
return 1 -- ALLOWED
else
return 0 -- THROTTLED
end
By executing the cleanup, count, and insertion in one atomic Lua script, race conditions are mathematically eliminated.
3. High-Scale Optimization: Local Batching & Token Synchronization
At 500,000 QPS, querying a central Redis cluster for every single incoming HTTP request introduces:
- Network round-trip latency ($1\text{ to }3\text{ ms}$ overhead per request).
- Centralized Redis cluster saturation.
The Batch Reservation Pattern (Stripe Architecture)
Instead of asking Redis for 1 token on every single request:
- Each API gateway pod asks Redis for a batch of 50 tokens at a time:
redis.decrby(quota_key, 50). - The gateway satisfies the next 50 user requests purely in local CPU memory with zero network latency ($<0.01\text{ms}$).
- When the local batch is exhausted, the pod reserves another batch.
- Result: Central Redis query volume is reduced by 98%, while still preventing unbounded quota exhaustion!