sehaj/learn
System design/14 Reliability, security and operations

14.04 · concept · 9 min

Where to put a rate limiter

Where limits are enforced decides what they can protect: at the edge, in the gateway, in each service, or in the client. Sharing counters across servers with Redis, local limits that need no network call, choosing what to key on, and failing open or closed.

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

WhereProtects againstKnows
Edge / CDNFloods and abuse before they reach youIP address, URL, headers
API gatewayPer-key and per-plan quotas, across all servicesAPI key, user, plan, route
Each serviceOverload of its own dependencies, expensive operationsEverything about the request
ClientIts own retries and burstsIts 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: INCR the 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

Docs

REST 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

2012

429 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 ↗
Predict first

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?

Commit to an answer before you read on.

Recap

Interview questions

SeniorfreqMust-knowDesign

Design a distributed rate limiter for an API served by many servers.

Report a mistakeEdit this page