Rate Limiting Algorithms Compared: Token Bucket, Leaky Bucket, Fixed Window
Rate limiting is the practice of restricting how many requests a client can make to an API within a time window. The algorithm used determines how requests are counted, when limits are enforced, and how burst traffic is handled. Common algorithms include fixed window (simple count per time period), sliding window (count across a rolling time period), token bucket (tokens regenerate at a fixed rate, bursts are allowed up to bucket capacity), and leaky bucket (requests are queued and processed at a fixed rate, smoothing burst traffic).
Written by Yashveer Singh, founder of Yashveer Labs.
What you need to know
- Fixed window rate limiting is simple to implement but vulnerable to boundary bursts. Sliding window or token bucket prevents the boundary exploit.
- Token bucket is the most commonly correct choice for public-facing APIs: it allows legitimate burst traffic while enforcing a steady-state rate.
- Rate limit state must be shared across all instances in a distributed system. Local in-memory counters produce per-instance limits instead of per-user limits.
- Return standard rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) so API clients can adapt their behavior rather than hitting limits blindly.
- Rate limits should be set by use case, not by system capacity. Distinguish between authenticated users, unauthenticated users, and internal services, each with different limits.
The core argument
Rate limiting is one of the features teams add quickly without thinking through the algorithm choice, then regret later when they discover their implementation allows boundary bursts or penalizes users unfairly. The algorithm choice is not academic. A fixed window rate limiter that resets at the top of every minute creates a predictable exploitation pattern where clients can double their effective rate by timing requests to the window boundary. A token bucket that regenerates tokens too slowly frustrates users who make legitimate but bursty requests.
The right framing for rate limit algorithm selection is: what traffic pattern do you want to accommodate? If the answer is steady traffic with occasional short bursts (the pattern for most interactive API usage), token bucket is correct. The bucket accumulates tokens during quiet periods and allows burst consumption when needed, while enforcing the steady-state rate over time. If the answer is processing requests at a fixed rate regardless of how they arrive (payment processing, email sending, message publishing), leaky bucket is correct. It queues excess requests and processes them at the configured rate.
For most B2B SaaS APIs I have built, token bucket per-user has been the right choice. The implementation on top of Redis is straightforward: store the last request timestamp and current token count per user key, regenerate tokens based on elapsed time since the last request, and deduct tokens atomically on each request. The sliding window log algorithm (storing each request timestamp in a sorted set with TTL) produces more accurate per-window counts but requires more Redis memory for high-volume users.
Common mistakes
- Implementing rate limiting in application code without shared state. In-memory rate limiters on individual application servers produce per-server limits rather than per-user limits. A user hitting three different instances can exceed the intended limit by 3x. Rate limit state belongs in Redis or another shared store.
- Setting the same limit for all API consumers. An authenticated enterprise customer making bulk API calls for a legitimate integration has different usage patterns than an unauthenticated scraper. Differentiate rate limits by authentication status, user tier, and endpoint type rather than applying a single global limit.
- Not returning informative error responses on limit exceeded. A 429 response without a Retry-After header or rate limit headers forces clients to implement arbitrary backoff. The Retry-After header tells clients exactly when they can retry. Good 429 responses produce well-behaved clients; opaque 429 responses produce aggressive retry loops.
- Rate limiting at the application layer when the gateway layer can do it. Application-layer rate limiting consumes server resources for requests that should be rejected before reaching the application. API gateways (Kong, Nginx, Cloudflare) can enforce rate limits at the infrastructure layer, returning 429 responses before the request touches application code.
- Not testing the rate limiter under burst conditions. A rate limiter that works correctly under steady traffic may fail under burst conditions due to race conditions in the token regeneration logic. Test with concurrent requests at the limit boundary to confirm the atomic operations work correctly under load.
Where to start
- Choose the algorithm based on the traffic pattern you want to allow. For interactive API users who make bursty requests: token bucket with a capacity of 2 to 5x the per-second steady-state rate. For background job processing with consistent rate requirements: leaky bucket. For simplicity at low scale: sliding window counter (avoids fixed window boundary issues without full token bucket complexity).
- Implement rate limit state in Redis with atomic operations. For token bucket: use a Lua script to atomically read the current token count, regenerate tokens based on elapsed time, deduct the request cost, and write back the new state. The atomicity prevents race conditions where two simultaneous requests both read the same token count and both succeed when only one should.
- Add rate limit headers to all API responses, not just 429 responses. Clients that can see their remaining request budget make better decisions about request pacing. A client that sees RateLimit-Remaining: 5 on a non-error response will slow down before hitting the limit. A client with no visibility into their budget hits the limit unexpectedly and retries aggressively.
Related reading
Frequently asked
Why you should hire Yashveer Singh for this
The kind of work this article describes is the kind of work I do every week. Production deployments, scaling decisions, the architecture choices that compound over years. I am Yashveer Singh, founder of Yashveer Labs. If you need this done, I do not need to be sold on the brief. Send me what you have and I will tell you what it actually takes.
Posts that line up with this one.
- Backend, APIs, and System Design
Idempotency Keys: A Pattern Every Senior Engineer Should Master
Idempotency keys are a small implementation with an outsized impact on system reliability. Here is the pattern, the edge cases, and the production pitfalls that most introductions skip.
- Backend, APIs, and System Design
JSON Columns in Postgres: When They Make Sense
JSON columns in Postgres are genuinely useful for flexible, semi-structured data. They are also frequently misused as a shortcut to avoid schema design. Here is when to use them and when to use normalized tables instead.
- Backend, APIs, and System Design
Kafka in 2026: When You Need It and When You Do Not
Kafka is powerful, but most startups reach for it before they need it. Here is how to decide.
- Backend, APIs, and System Design
Lambda Cold Starts: Why They Still Matter in 2026
Cold starts have improved significantly but have not been eliminated. Here is the current state of cold start latency, which use cases still require mitigation, and the practical patterns that keep them from affecting users.