Distributed Rate Limiting with Redis: Token Bucket and Sliding Window Logs
Protecting public APIs against denial-of-service and credential stuffing with sub-millisecond atomic Lua scripts.
1. Why Naive Rate Limiting Falls Apart Under Load
The first rate limiter most people write is an in-process counter: a Map keyed by user ID or IP, incremented per request, reset every minute. It works in a demo and falls apart the moment you run more than one instance of your API - each process has its own counter, so a limit of 100 requests/minute becomes 100 times N instances, silently, with no error and no log line telling you why. This is the single most common reason 'rate limiting' in production does nothing at all.
The fix is to move the counter state somewhere every instance shares - which in practice means Redis, because the operation needs to be both fast (sub-millisecond, on the hot path of every request) and atomic (two concurrent requests from the same client must not both read the same stale count and both get admitted). A round trip to Postgres for this is possible but adds latency and load you don't need to pay on every single request.
Atomicity is the part naive implementations get wrong even after moving to Redis. A read-then-write pattern (GET count, check threshold, INCR) executed as two separate Redis commands has a race window: two requests arriving in the same microsecond can both read the same value below the limit and both proceed, silently doubling your effective limit under concurrent load. The fix is doing the check-and-increment as a single atomic operation.
2. Token Bucket vs Sliding Window: Choosing the Right Algorithm
Token bucket models a bucket that holds up to N tokens, refilled at a fixed rate (e.g. 10 tokens/second), where each request consumes one token and is rejected if the bucket is empty. Its defining property is that it allows bursts: a client that's been idle can spend its full bucket in a single instant, then has to wait for refill. This matches real traffic patterns well - a user clicking through a UI in a burst of five clicks shouldn't get throttled just because it happened in the same second.
Sliding window (log or counter variant) instead tracks requests within a strictly moving time window - 'no more than 100 requests in the last 60 seconds, measured continuously, not reset on the minute boundary.' Fixed windows have a well-known edge-case flaw: a client can send 100 requests at 0:59 and another 100 at 1:01, technically respecting a 100/minute fixed-window limit while bursting 200 requests in two seconds. Sliding window closes that gap by evaluating the limit continuously rather than at fixed boundaries.
The practical choice: token bucket for smoothing legitimate bursty client behavior (typing indicators, batched UI actions) where you want to allow short spikes; sliding window for hard security boundaries (login attempts, password reset requests) where you specifically want to prevent boundary-gaming. Many production systems layer both - a generous token bucket per user for general API traffic, and a strict sliding window on specific sensitive endpoints.

