Rate limiting
In one line
A rate limiter caps how many requests a client can make in a period, protecting services from abuse and overload, usually with a token bucket or sliding window counter kept in a fast shared store.
What it is
Rate limiting restricts how often a client (identified by API key, user id or IP address) can call an API, for example 100 requests per minute. Requests over the limit are rejected, typically with HTTP 429 Too Many Requests and a Retry-After header.
It protects against abuse (brute-force logins, scraping), keeps one noisy customer from starving others, controls costs of expensive operations, and enforces pricing tiers. It can run in the client, at an API gateway or edge, or inside each service.
Why interviewers ask
'Design a rate limiter' is a common interview question in its own right, and rate limits come up in almost every API design. Interviewers look for knowledge of the algorithms and their trade-offs, how to make limits work across many servers, and what happens when the limiter's store fails.
How it works
- 1Identify the client for each request (API key, user id, or IP for anonymous traffic) and look up its rule, such as 10 requests per second with bursts up to 20.
- 2Token bucket: each client has a bucket of capacity C that refills at R tokens per second. Each request takes one token; if the bucket is empty the request is rejected. This allows short bursts up to C while enforcing an average of R.
- 3Leaky bucket: requests enter a queue that drains at a fixed rate, smoothing output; a full queue rejects new requests.
- 4Fixed window counter: count requests per calendar window (for example per minute). Simple, but a client can send up to twice the limit around a window boundary (end of one minute plus start of the next).
- 5Sliding window log stores each request timestamp and counts those in the last 60 seconds: exact but memory-heavy. Sliding window counter estimates with two counters: count = current window + previous window x (fraction of the previous window still inside the sliding range).
- 6In a distributed system, keep counters in a shared store like Redis and update them atomically (a Lua script or INCR with EXPIRE) so all API servers see the same count.
- 7Return 429 with Retry-After and rate-limit headers, and decide fail-open (allow traffic if the limiter is down) or fail-closed (reject) per endpoint.
Worked example · Token bucket with capacity 5, refilling 1 token per second
- t = 0.0 s: bucket is full with 5 tokens. The client sends 5 requests at once; each takes a token. All 5 allowed, bucket = 0.
- t = 0.5 s: a 6th request arrives. Only 0.5 tokens have refilled, less than 1, so it is rejected with 429.
- t = 2.0 s: 2 tokens have refilled (bucket = 2). Two requests arrive: both allowed, bucket = 0.
- t = 12.0 s: 10 seconds idle would add 10 tokens, but the bucket caps at 5. A burst of 6 requests: 5 allowed, the 6th rejected.
- Over a long time the client averages at most 1 request per second, but can burst up to 5.
Picture it
A token bucket is an arcade card that gets one credit every second, up to a maximum of five. You can play five games in a row after a break, but then you have to wait for credits to come back.
Reference code
import time
class TokenBucket:
def __init__(self, capacity, refill_per_sec):
self.capacity = capacity
self.rate = refill_per_sec
self.tokens = capacity
self.last = time.monotonic()
def allow(self):
now = time.monotonic()
# Refill lazily for the time elapsed, never above capacity
self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
self.last = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
# One bucket per client key
buckets = {}
def is_allowed(client_id, capacity=5, rate=1.0):
bucket = buckets.setdefault(client_id, TokenBucket(capacity, rate))
return bucket.allow()Key patterns
Token bucket
When: APIs that should allow short bursts but cap the average
How: Store tokens and last refill time per key; refill lazily on each request based on elapsed time.
Sliding window counter
When: Smooth per-minute limits without per-request memory
How: Keep counts for the current and previous window and weight the previous one by overlap.
Leaky bucket
When: You need a steady output rate to protect a fragile downstream
How: Queue requests and process them at a fixed rate; reject when the queue is full.
Layered limits
When: Different threats at different levels
How: Combine per-IP limits at the edge, per-key limits at the gateway and per-endpoint limits for expensive operations like login or search.
Real-world use
Stripe API
Stripe has described using token bucket rate limiters backed by Redis, plus load shedders that drop lower-priority traffic first when the system is under stress.
GitHub REST API
Authenticated requests are limited per hour (5,000 for a standard user token), and every response includes headers such as X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
NGINX limit_req
Implements a leaky bucket per key (often client IP), with an optional burst queue, so it is a common first line of defence in front of web apps.
Cloudflare and API gateways
Cloudflare, AWS API Gateway and similar services enforce rate limits at the edge so abusive traffic is dropped before it reaches your servers.
In a project
A public REST API for a mapping service is being hammered by a few scrapers, causing timeouts for paying customers.
- Define tiers: free keys get 10 requests per second with bursts of 20, paid keys 100 per second with bursts of 200, and anonymous IPs 1 per second.
- Implement a token bucket per API key in Redis using a Lua script that refills based on elapsed time and takes a token atomically.
- Run the check in the API gateway so rejected requests never reach the expensive map-rendering servers.
- Return 429 with Retry-After and X-RateLimit-Remaining headers, and document them so client SDKs back off.
- Fail open if Redis is unreachable for the general API, but keep a local in-memory fallback limit per server so traffic cannot become unbounded.
Impact: Scraper traffic drops by 95%, paying customers' p99 latency recovers from 3 s to 150 ms, and upgrade requests rise because the free tier limit is now clear and enforced.
Trade-offs
Use it when
- Public APIs, login and signup endpoints, expensive operations like search or exports
- Multi-tenant systems where one customer must not starve others
- Protecting a downstream service with a known capacity
Avoid it when
- Using rate limiting as the only overload protection; also use autoscaling, timeouts and load shedding
- Limiting by IP alone where many users share one address (offices, mobile carriers), which blocks innocent users
- Fixed windows when boundary bursts of up to 2x the limit would hurt the backend
Common mistakes
- Keeping counters in each server's memory, so with 10 servers a client effectively gets 10x the limit.
- Using non-atomic read-then-write on Redis, letting concurrent requests slip past the limit.
- Forgetting the fixed window boundary problem.
- Not returning 429 with Retry-After, so clients retry immediately and make overload worse.
- Not deciding what happens when the limiter's store is down.
Interview questions
Quick revision
- Reject over-limit requests with HTTP 429 + Retry-After.
- Token bucket: capacity C (burst), refill R/s (average).
- Leaky bucket: fixed drain rate, smooths output.
- Fixed window: simple, up to 2x burst at boundaries.
- Sliding counter: current + previous x overlap fraction.
- Distributed: shared Redis counters, atomic Lua or INCR + EXPIRE.
- Decide fail-open vs fail-closed per endpoint.
Check yourself
1. A token bucket has capacity 10 and refills 2 tokens per second. It is full and idle. What is the largest burst the client can send at once?
2. With a fixed window limit of 100 requests per minute, how many requests can a client get through within about 2 seconds around a window boundary?
3. Why should a distributed rate limiter update its counter atomically?
4. Which HTTP status code is standard for a rate-limited request?
See it in a full design
These walkthroughs use this lesson.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.