Rate Limiting Algorithms: Token Bucket, Leaky Bucket and Sliding Windows
How rate limiting works — fixed window, sliding log, sliding window counter, token bucket and leaky bucket algorithms, distributed rate limiting with Redis, headers and best practices.
Rate limiting controls how many requests a client can make in a period of time. It protects your services from overload and abuse, keeps usage fair across customers, controls costs (especially for expensive operations like LLM calls), and enforces pricing tiers. Every public API needs it, and most internal services benefit too.
Key takeaways
- Rate limiting protects availability, ensures fairness, defends against abuse and controls cost.
- The main algorithms: fixed window, sliding log, sliding window counter, token bucket and leaky bucket.
- Token bucket is the most popular for APIs: it allows short bursts while enforcing an average rate.
- In distributed systems, keep counters in a shared store such as Redis, and update them atomically.
- Return 429 Too Many Requests with
Retry-Afterand rate-limit headers so clients can behave.
Where to rate limit
- Edge / API gateway: coarse protection per IP or API key before traffic reaches your services.
- Application layer: fine-grained limits per user, tenant, endpoint or operation cost.
- Outbound: limit your own calls to third-party APIs to stay within their quotas.
Choose the key carefully: per API key or user is fairer than per IP (many users can share one IP behind a NAT, and one attacker can rotate IPs).
1. Fixed window counter
Count requests in fixed time buckets, such as per minute:
textWindow 12:00:00–12:00:59 → count ≤ 100
Window 12:01:00–12:01:59 → count resets
- ✅ Simple and memory-cheap (one counter per key per window).
- ❌ Boundary bursts: a client can send 100 requests at 12:00:59 and another 100 at 12:01:00, so 200 in two seconds.
2. Sliding window log
Store a timestamp for every request; count those within the last N seconds.
- ✅ Precise, with no boundary bursts.
- ❌ Memory-heavy at high volumes (one entry per request).
3. Sliding window counter
Approximate a sliding window by weighting the previous fixed window's count:
textestimated = current_window_count + previous_window_count × (1 − elapsed_fraction_of_current_window)
At 25% into the current minute, with 40 requests so far and 80 in the previous minute: 40 + 80 × 0.75 = 100.
- ✅ Smooths boundary bursts, and cheap (two counters).
- ⚠️ Approximate, which is fine for most use cases.
4. Token bucket
A bucket holds up to capacity tokens and refills at a steady rate. Each request consumes a token; if the bucket is empty, the request is rejected (or delayed).
textcapacity = 20 tokens, refill = 5 tokens/second
→ a client can burst 20 requests instantly, then sustain 5/second
pythonimport time
class TokenBucket:
def __init__(self, capacity: float, refill_per_sec: float):
self.capacity = capacity
self.tokens = capacity
self.rate = refill_per_sec
self.updated = time.monotonic()
def allow(self, cost: float = 1.0) -> bool:
now = time.monotonic()
self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.rate)
self.updated = now
if self.tokens >= cost:
self.tokens -= cost
return True
return False
- ✅ Allows natural bursts (page loads that fire several API calls) while enforcing an average rate.
- ✅ Cost-based limiting: expensive operations consume more tokens.
- ✅ Only two values per key (tokens, last update time).
This is why token bucket is the default in many API gateways and cloud services.
5. Leaky bucket
Requests enter a queue (the bucket) that drains at a constant rate. If the queue is full, new requests are dropped.
- ✅ Produces a smooth, constant output rate. Useful for protecting a downstream system that can't handle bursts.
- ❌ Adds latency (queuing), and bursts are smoothed rather than served quickly.
Comparison
| Algorithm | Bursts | Precision | Memory | Typical use |
|---|---|---|---|---|
| Fixed window | Boundary spikes | Low | Very low | Simple quotas (daily limits) |
| Sliding log | None | Exact | High | Low-volume, strict limits |
| Sliding window counter | Smoothed | Approximate | Low | General API limits |
| Token bucket | Allowed up to capacity | Good | Low | APIs, user-facing limits |
| Leaky bucket | Smoothed via queue | Good | Queue size | Traffic shaping, protecting downstream |
Distributed rate limiting with Redis
With many app instances, in-memory counters don't work, because each instance sees only part of the traffic. Use a shared store and atomic operations. A fixed-window example:
pythondef allow(redis, key: str, limit: int, window_s: int) -> bool:
bucket = f"rl:{key}:{int(time.time() // window_s)}"
pipe = redis.pipeline()
pipe.incr(bucket)
pipe.expire(bucket, window_s + 1)
count, _ = pipe.execute()
return count <= limit
For token buckets or sliding windows, implement the logic in a Lua script so the read-modify-write happens atomically on the Redis server.
Considerations:
- Latency: a Redis round trip per request. Keep Redis close, and consider local pre-checks for very high throughput.
- Failure mode: if Redis is down, do you fail open (allow traffic, which risks overload) or fail closed (reject traffic, which risks an outage)? Usually fail open with a conservative local limiter as backup.
- Hot keys: a single very active key can concentrate load on one Redis shard.
Communicating limits to clients
httpHTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 12
Content-Type: application/json
{"error": {"code": "rate_limited", "message": "Too many requests. Retry in 12 seconds."}}
- Include limits in successful responses too, so clients can pace themselves.
- Document limits per plan and endpoint (API design best practices).
- Well-behaved clients should back off exponentially with jitter. That applies to your own scrapers and integrations, too (responsible web scraping).
Designing good limits
- Layer limits: per second (burst protection) plus per day (quota).
- Different limits per endpoint: logins and password resets need strict limits to prevent credential stuffing; read endpoints can be generous.
- Weight by cost: an export or an LLM-backed endpoint should consume more quota than a simple read (LLM cost optimization).
- Tenant fairness: prevent one customer from starving others in multi-tenant systems.
- Load shedding: beyond per-client limits, shed low-priority traffic when the system is overloaded (how to scale a web application).
FAQ
What is the best rate limiting algorithm?
For most APIs, token bucket: it allows reasonable bursts while enforcing an average rate, supports cost-based limits, and is cheap to implement. Sliding window counters are a good alternative when you want smoother enforcement of a strict per-window limit.
Should rate limiting be done at the API gateway or in the app?
Both. Gateways provide coarse, early protection (per IP or key). The application enforces fine-grained, business-aware limits (per user, tenant, operation cost).
What's the difference between rate limiting and throttling?
The terms overlap. Rate limiting typically rejects requests over the limit (429). Throttling often implies slowing requests down, by queuing or delaying them, rather than rejecting them.
How do I rate limit by IP safely behind a load balancer?
Use the client IP from trusted forwarding headers set by your own load balancer or CDN, and don't trust headers supplied directly by clients. Prefer API-key or user-based limits for authenticated traffic.