Skip to main content
PProfileMatch

Rate Limiting Explained: Why, Where and What to Limit

System Design & ArchitectureLesson 1 of 46 min read

What you'll learn

  • Name the problems rate limiting solves and the ones it does not
  • Choose between edge, gateway and in-service enforcement for a given need
  • Pick a sensible key (IP, user, API key, tenant, route) and explain its failure modes
  • Distinguish rate limits, quotas and load shedding

On this page

  1. What a rate limit actually protects
  2. Where to enforce it
  3. What to key on: the identity problem
  4. A first limit in a reverse proxy
  5. Common mistakes
  6. How this shows up in real projects and interviews

Imagine a pizza chain ships a mobile build with one small bug: when the order-status call fails, the app retries immediately, forever. A brief server blip now turns every open app into a machine-gun that fires as fast as the network allows, the servers get slower, more calls fail, more retries fire, and the outage feeds itself. No attacker was involved. A rate limit is the guard that stops one misbehaving client, malicious or merely buggy, from consuming capacity everyone else needs.

What a rate limit actually protects

A rate limit caps how many requests an identified caller may make in a period of time. Everything else is detail. The reasons teams add one fall into a few groups, and it helps to know which one you are solving because it decides the rest of the design.

  • Capacity and stability: keep a spike from one caller from exhausting CPU, database connections or a downstream dependency (the retry storm above).
  • Fairness: stop one heavy tenant from degrading the experience of all the others on shared infrastructure.
  • Abuse resistance: slow down credential guessing on a login route, scraping, or one-time-code flooding. Limits slow attackers; they do not stop a determined one, and they are not a replacement for real authentication and monitoring.
  • Cost control: paid downstream calls (SMS, maps, a model API) can turn a loop into a bill.
  • Commercial plans: free, pro and enterprise tiers often differ mainly in their limits.

Where to enforce it

Requests pass through several layers on the way to your code, and each layer can enforce limits with different trade-offs. The cheaper and earlier you reject, the less capacity a bad request costs. The later you reject, the more you know about who is calling and what they are asking for.

LayerWhat it can seeGood forWeakness
CDN / WAF edgeIP address, URL, headersVolumetric floods, obvious bots, cheap rejection far from your serversKnows nothing about your users or plans
API gateway / reverse proxyIP, API key or token, routeA uniform per-key policy for every service behind itCoarse; cannot see business context such as account size
Inside the serviceAuthenticated user, tenant, request cost, business statePer-tenant plans, expensive-operation limits, per-account login attemptsThe request has already consumed a connection and some CPU
Client side (outbound)Your own calls to someone else's APIStaying under a partner's limits; being a polite callerOnly protects the callers who cooperate

Most mature setups layer them: the edge drops floods, the gateway applies a per-key ceiling, and the service enforces the rules only it can know, such as "five failed password attempts per account per hour". For how the layers fit into a bigger picture, see Scalability and Load Balancing.

What to key on: the identity problem

A limit is meaningless without a key: the identity whose requests are counted together. The choice has consequences that surprise people. The next program counts requests per key for the same traffic twice: four colleagues behind one office network address (NAT) and one script with its own key.

keys.py
from collections import defaultdict

LIMIT = 5  # requests allowed per window, per key


def run(key_fn, requests):
    counts = defaultdict(int)
    verdicts = defaultdict(list)
    for req in requests:
        key = key_fn(req)
        counts[key] += 1
        verdicts[req["user"]].append("200" if counts[key] <= LIMIT else "429")
    return dict(verdicts)


# Four colleagues share one office IP (NAT), two requests each.
office = [
    {"user": u, "ip": "203.0.113.10", "api_key": "key-" + u}
    for u in ["asha", "ben", "chitra", "dev"]
    for _ in range(2)
]
# One script on its own IP sends 8 requests with a single key.
bot = [{"user": "bot", "ip": "198.51.100.7", "api_key": "key-bot"} for _ in range(8)]
traffic = office + bot

for label, fn in [("IP", lambda r: r["ip"]), ("API key", lambda r: r["api_key"])]:
    print("keyed by", label)
    for user, codes in run(fn, traffic).items():
        print(f"  {user:7}", " ".join(codes))

Output

keyed by IP
  asha    200 200
  ben     200 200
  chitra  200 429
  dev     429 429
  bot     200 200 200 200 200 429 429 429
keyed by API key
  asha    200 200
  ben     200 200
  chitra  200 200
  dev     200 200
  bot     200 200 200 200 200 429 429 429

Keyed by IP, innocent colleagues Chitra and Dev were rejected because their office shares an address; keyed by API key, only the script was limited. The reverse failure exists too: if you key purely on an identity that is cheap to create (a free sign-up, a rotating key), an abuser simply makes more of them. Each key type fails differently.

KeyWorks well whenBreaks when
IP addressCallers are anonymous (login page, sign-up, public search)Many users share an IP (offices, mobile carriers), or one attacker rotates addresses
User or account idCallers are authenticatedAnonymous routes; attackers who create many accounts
API key / tokenMachine clients and public APIsKeys leaked or shared; key farming on free tiers
Tenant / organisationB2B products with fair-share concernsA tenant's own users need individual limits as well
Route or operationExpensive endpoints (report export, password reset)Used alone, it ignores who is calling

Real policies usually combine two keys, such as per-IP and per-account on a login route. And a warning about IP keys behind proxies: you must take the client address from a header like X-Forwarded-For only from proxies you operate, and use the address those proxies added, not whatever the client sent, otherwise anyone can pick their own identity by setting the header.

A first limit in a reverse proxy

For a quick, first layer you do not need to write code. This nginx excerpt limits each client IP on a login route to about 5 requests per second with a short burst allowance. It is not run here, and the directive names are worth checking against your nginx version. Note limit_req_status: without it nginx answers rejected requests with 503, which looks like a server fault rather than a rate limit.

nginx.conf (excerpt)
http {
    limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/s;

    server {
        location /login {
            limit_req zone=login_zone burst=10 nodelay;
            limit_req_status 429;
            proxy_pass http://app_backend;
        }
    }
}

Common mistakes

  • Limiting only by IP for a B2B product. A customer whose staff share an office gateway starts getting 429s together. Symptom: support tickets from one company all at the same hour.
  • Trusting a client-supplied forwarding header. Anyone can send X-Forwarded-For: 1.2.3.4 and slip past a per-IP limit. Symptom: a limit that works in tests but not against a script that rotates the header.
  • Returning 503 or 500 for a rejection. Clients and monitors then treat it as a server outage and retry harder. Use 429 and say when to come back.
  • Applying one global number to every route. A static asset and a report export have wildly different costs. Symptom: either the cheap route is over-restricted or the expensive one is unprotected.
  • Forgetting that the limiter is on the hot path. A limiter that does a slow database query per request becomes the bottleneck it was meant to prevent.

How this shows up in real projects and interviews

"Design a rate limiter" is a staple system design question, and a strong answer starts with exactly this lesson's questions before drawing boxes: who is the caller, what are we protecting, where do we enforce, what do we do when the limit is hit. In real projects, the first limit is usually added after an incident, so a good habit is to ask during design which routes are expensive and which callers are unauthenticated, and protect those first. Our system design interview guide covers how to frame such a question.

Try it yourself

Take an API you know (or invent a ticket-booking one) and list four endpoints. For each, decide the key (IP, user, API key, tenant) and the layer that should enforce the limit, and write one sentence on who would be unfairly blocked if you chose a different key. Then extend keys.py with a fifth user who rotates through several API keys and observe how a per-key limit behaves.

Show a hint

Anonymous endpoints like sign-up and login can only be keyed by IP at first; authenticated ones should prefer the user or tenant. For the rotating-key user, add a third key function that groups by something harder to change.

Key takeaways

  • A rate limit caps requests per identified caller per period; quotas cap long-term volume and load shedding protects the server regardless of caller.
  • Enforce early and cheaply at the edge, per key at the gateway, and with business context inside the service; layered limits are normal.
  • The key decides who is punished: IP keys hurt shared networks, account keys miss anonymous routes, and cheap identities can be multiplied.
  • Take client addresses only from proxies you control, and answer rejections with 429 rather than 5xx.
  • Start by protecting expensive routes and unauthenticated entry points such as login and password reset.

Test yourself

A trick from this lesson's ride: Rate is speed, quota is mileage, shedding is the shop saying "we are full". Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What is rate limiting in an API?

Rate limiting caps how many requests a particular client can make to an API in a given time window, such as 100 per minute. Requests beyond the cap are rejected, typically with HTTP 429, or delayed. It protects capacity, keeps shared services fair and slows abuse like password guessing.

Should I rate limit by IP address or by user?

Use the user, account or API key whenever the caller is authenticated, because many people can share one IP behind NAT. Use IP for anonymous routes like login and sign-up, and often combine both. Only trust forwarded-IP headers that your own proxies set.

What is the difference between rate limiting and throttling?

The terms overlap and are used loosely. Rate limiting usually means rejecting requests over a cap, while throttling often means slowing or queueing them so they are processed at a controlled pace. Many gateways use the words interchangeably, so check how a specific product behaves.