The problem: 40 servers, one limit
Snip's limit is "100 link creations per minute per API key". But requests from one key are spread across 40 app servers by the load balancer. If each server keeps its own count, the key really gets 4,000 a minute. Where the counting happens, and what it's keyed on, matters as much as the algorithm.
The layers
| Where | Protects against | Knows |
|---|---|---|
| Edge / CDN | Floods and abuse before they reach you | IP address, URL, headers |
| API gateway | Per-key and per-plan quotas, across all services | API key, user, plan, route |
| Each service | Overload of its own dependencies, expensive operations | Everything about the request |
| Client | Its own retries and bursts | Its own behaviour |
Most systems use several. Snip limits by IP at the CDN (stopping floods of redirects or link creation from one address), by API key and plan in the gateway (100 link creations a minute on the basic plan), and inside the link service on the database's capacity (a cap on concurrent writes, 03.11 Backpressure and load shedding).
Sharing counts across servers
For a global "100 per minute per key", the count has to be shared. The usual store is Redis, with the check done atomically on the server so two app servers can't both read 99 and both allow:
- A fixed or sliding window counter:
INCRthe current window's key and set an expiry, in one script, compare with the limit. - A token bucket: a short script that reads the bucket, refills it for the elapsed time, takes a token and writes it back, all as one atomic step.
That adds a network round trip (around half a millisecond, 01.04 Numbers to know) to every limited request, and Redis becomes a dependency of the API.
Local limits: no round trip
Alternatives trade precision for speed:
- Divide the limit. With 40 servers, each allows 100 ÷ 40 per key. No coordination, but uneven load balancing makes it inaccurate, and it changes whenever the fleet resizes.
- Local buckets, synced periodically. Each server counts locally and reconciles with a shared store every second or so. Limits are approximately right, overshooting briefly.
For abuse protection, approximate is fine; for billing quotas ("10,000 API calls a month"), use the shared, exact counter.
What to key on
- API key or user: the normal choice for authenticated APIs. Fair and hard to evade.
- IP address: for unauthenticated traffic. Many users can share one IP (offices, mobile carriers), and attackers can rotate through many, so set it generously and combine it with other signals.
- Route or cost: creating a link costs far more than reading one; weight requests by cost, or give expensive routes their own limit.
- Hot keys: one enormous customer's counter in Redis is a hot key (05.02 Hot partitions and celebrity keys); a local pre-check before the shared one keeps it off the shard.
When the limiter is down
If Redis is unreachable, should requests be allowed (fail open) or rejected (fail closed)? For most APIs, fail open: a brief window without per-key limits is better than rejecting every customer, and the service's own concurrency limits still protect it. Fail closed only where the limit protects something critical, such as a costly third-party quota.
Seen in production
GitHub
DocsREST API rate limits
Authenticated requests count towards a personal limit of 5,000 requests per hour, and unauthenticated ones are limited to 60 per hour. Every response carries x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-used and x-ratelimit-reset. Separate secondary limits cap, for example, concurrent requests at 100 and REST usage at 900 points per minute, and can change without notice.
What we foundPer-user primary limits with headers on every response, plus secondary limits on concurrency and cost.
Rate limits for the REST API (GitHub documentation)docs.github.com ↗IETF
2012429 Too Many Requests
RFC 6585 defines 429 Too Many Requests: the user has sent too many requests in a given amount of time ("rate limiting"). The response may include a Retry-After header indicating how long to wait before making a new request.
What we foundThe standard status code for rate limiting, optionally with Retry-After.
RFC 6585: Additional HTTP Status Codes (Nottingham and Fielding)rfc-editor.org ↗Snip enforces '100 per minute per API key' with an in-memory counter on each of 40 app servers. A customer reports they can make 3,000 requests a minute. Why, and what's the fix?
Recap
Interview questions
Design a distributed rate limiter for an API served by many servers.