Why rate limit
Every API has finite capacity, and not every caller is well behaved. A scraper, a buggy client stuck in a retry loop, or a burst of legitimate traffic can exhaust your database connections and take the service down for everyone. Rate limiting caps how fast a client may make requests so no single caller can consume the whole system.
It also makes costs predictable, enforces fair use across tenants and plans, and gives you a lever to slow abuse before it becomes an outage.
What to limit
A limit is only meaningful relative to an identity. Common choices:
- API key — the best option for server-to-server and third-party clients.
- User id — natural for authenticated apps.
- IP address — a fallback for anonymous traffic, but many users can share one.
- Combination — key plus IP, or user plus endpoint, for precision.
- Endpoint or tier — stricter limits on expensive operations like search or exports.
Also decide the scope: a global limit, a per-endpoint limit, or both. A global limit protects the service; per-endpoint limits protect specific expensive work.
Algorithms
Four algorithms cover almost every need.
Fixed window — count requests in a fixed interval and reset at the boundary.
limit: 100 per minute
key: client:123:2026-09-15T10:05
It is cheap and simple, but a client can send 100 requests at the end of one window and 100 at the start of the next, effectively doubling the rate at the boundary.
Sliding window — count over the last N seconds rather than a fixed block, usually by combining the current and previous window with a weighted average. Smoother than fixed window with similar cost.
Token bucket — a bucket refills at a steady rate, and each request consumes a token. It allows short bursts up to the bucket size while enforcing an average rate, which matches how real clients behave.
// token-bucket.js
const capacity = 20;
const refillPerSecond = 5;
let tokens = capacity;
let last = Date.now();
function allow() {
const now = Date.now();
tokens = Math.min(capacity, tokens + ((now - last) / 1000) * refillPerSecond);
last = now;
if (tokens < 1) return false;
tokens -= 1;
return true;
}
Leaky bucket — requests enter a queue that drains at a fixed rate. It smooths traffic to a constant output, useful when downstream systems need steady load.
Responding to limits
Tell clients what is happening with standard signals.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
- 429 Too Many Requests is the correct status code.
- Retry-After tells the client when to try again, in seconds or a date.
- RateLimit-* headers expose the limit, remaining count and reset time so clients can pace themselves.
Returning 200 or silently dropping requests hides the problem and leads to mysterious client bugs. Well-behaved clients read Retry-After and back off automatically.
Distributed limiting
If you run more than one instance, counters must be shared. In-memory limits apply per instance, so with ten servers a client effectively gets ten times the limit.
// redis.js
const key = `ratelimit:${apiKey}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
if (count > 100) {
// reject with 429
}
Use atomic operations, or a library that uses a Lua script, so increments and expiries are race-free. Redis is the usual store because it is fast and supports atomic scripts and expiry. API gateways and edge platforms can enforce limits before traffic even reaches your application, which is the most efficient place to do it.
Quotas versus rate limits
They solve different problems and often coexist.
- Rate limit — how fast: 100 requests per minute.
- Quota — how much: 100,000 requests per month, tied to a plan.
A client can stay under its rate limit all month and still exhaust its quota. Track quotas separately, usually with a longer-lived counter, and return a distinct error when a quota is exhausted so clients know the difference.
Avoiding harm to real users
Limits should stop abuse without punishing normal usage.
- Set limits from measured traffic, with headroom for bursts.
- Allow bursts with a token bucket rather than a hard cliff.
- Use different limits per tier and per endpoint.
- Exclude health checks, internal calls and static assets.
- Prefer temporary slowdowns to permanent bans.
- Monitor rejection rates and tune; a spike in 429s may mean a limit is too tight.
Best practices
- Identify clients with the most specific stable key available.
- Choose an algorithm that matches the traffic shape.
- Return 429 with
Retry-Afterand rate limit headers. - Share counters in Redis or enforce limits at the edge.
- Separate rate limits from quotas and expose both.
- Allow bursts and set limits from real measurements.
- Log and monitor rejections, and alert on sudden spikes.
Common mistakes
- Counting per instance and multiplying the effective limit.
- Using only IP, which punishes users behind shared NAT.
- Returning 200 or dropping requests silently.
- Setting limits so tight that normal clients are blocked.
- Forgetting to expire counters and leaking memory.
- Treating rate limits as a substitute for authentication and authorization.
Where to go next
Rate limiting keeps an API available under pressure. Ground it in REST design and the HTTP guide, pair it with API Versioning as part of the lifecycle, and implement it on Node.js. Then add a limiter to one endpoint and watch how it behaves under a load test.