What Is Rate Limiting? 7 Things Every Developer Should Know (With Examples)

October 26, 2026 · Web Development

Every public API has a breaking point, and rate limiting is how it stays on the right side of it. What is rate limiting exactly? It is a cap on how many requests one client may make in a given time window — an illustrative policy is 100 requests per minute per API key. Cross the cap and the server refuses further requests, usually with a 429 Too Many Requests response, until the window resets. This guide explains the four main algorithms, the headers that make limits visible, and how to behave on both sides of the limit.

Illustration of many client devices sending request dots through a gate that lets only some through to a server
The core idea: a gate between clients and the server, letting requests through up to a quota and turning the rest away.

1. Why limits exist at all

An API without limits trusts every client to behave, and clients do not always behave. A buggy loop can fire thousands of requests a second, a scraper can walk an entire catalogue overnight, and a login endpoint without a limit invites password guessing. Even honest traffic hurts when it is uneven: one bulk sync at noon can starve every other user.

Limits solve four problems: stability, fairness, cost, and security — keeping load within capacity, stopping one client from hogging a shared resource, bounding billed work, and slowing brute force and scraping. The numbers differ by case: a login endpoint might allow a handful of attempts per quarter-hour, while a data endpoint might allow hundreds per minute.

2. The 429 response: what the client actually sees

When a client crosses its quota, the standard answer is 429 Too Many Requests, defined in RFC 6585, section 4. It is a client error, but it does not mean the client is broken: it means "you, specifically, are going too fast right now". Our HTTP status codes cheat sheet places 429 among the other 4xx codes — a 429 is about one client's quota, while a 503 says the service itself cannot cope, for anyone.

A good 429 carries a Retry-After header saying how long to wait, and an error body saying which limit was hit. Retry-After takes a delay in seconds (Retry-After: 60) or an HTTP date. The client-side rule is absolute: respect it — every rejected retry is still work, and retrying immediately turns a brief throttle into a longer one.

Limits should also be visible before they are hit, through headers on successful responses. The widely copied convention is the X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset trio: the quota, how many requests remain, and when the window resets. But that trio is a convention, not a standard — the reset value is a Unix timestamp in some APIs and a delta in seconds in others. The IETF draft for RateLimit-* headers aims to end that inconsistency, but it is still a draft, not a finished standard. Send the familiar trio and document what your reset value means.

3. Four algorithms, and the trade-off each one makes

Fixed window is the simplest: count requests in the current window, reset the counter when the window ends. Its flaw is the boundary burst: a client can send 100 requests in the last second of one window and 100 more in the first second of the next — 200 requests in about two seconds — without ever breaking the counter. For internal services that may be acceptable; for a public API under attack, it is not.

Sliding window fixes the boundary by counting requests in the last N seconds, whatever the clock says. That removes the boundary burst, but it costs memory — the server must remember individual request timestamps instead of one counter per client.

Diagram of a timeline split into two time windows, with a burst of dots at the boundary between them
The fixed-window flaw: the counter resets at the boundary, so a burst split across two windows sails through.

Token bucket is the most flexible of the four. Tokens drip into a bucket at a fixed rate, up to a maximum size, and each request spends one token; an empty bucket refuses the request. A full bucket holds spare tokens, so a short burst gets through, while the long-run average never exceeds the refill rate. A bucket of size 10 refilling at 1 token per second allows a 10-request burst, then at most 1 request per second afterwards — which suits interactive APIs, where a user legitimately clicks ten things at once.

import time

class TokenBucket:
    def __init__(self, rate, capacity):
        self.rate, self.capacity, self.tokens = rate, capacity, capacity
        self.last_refill = time.monotonic()

    def allow(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity,
                          self.tokens + elapsed * self.rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

bucket = TokenBucket(rate=1, capacity=3)
print([bucket.allow() for _ in range(5)])
# [True, True, True, False, False] — verified by running it

Leaky bucket works the other way round: requests pour in, but the bucket drains at a fixed rate, so output is smooth no matter how bursty the input is. It forbids bursts and enforces a steady pace. Its cost is latency — when the bucket is full, new requests wait in a queue or are refused, and an unbounded queue just converts a rate problem into a latency problem.

AlgorithmBursts allowed?CostBest for
Fixed windowYes, at boundariesOne counterSimple internal limits
Sliding windowNoTimestamp storagePublic APIs needing accuracy
Token bucketYes, up to bucket sizeSmall stateInteractive APIs, bursty users
Leaky bucketNoQueue managementSteady downstream systems
Diagram of a bucket filling with tokens from a drip, with one token leaving for each request
Token bucket: tokens refill steadily, each request spends one, and a full bucket is what makes short bursts legal.

4. What to limit by: the key is half the design

A limit is only as fair as its key — the identifier the counter is attached to. IP-based limits are the only option for unauthenticated endpoints, but they are blunt: our IP address guide explains why, since many users can share one public address behind NAT, so one heavy household can throttle a whole building.

API keys and user accounts are fairer keys, because they track the party actually making the calls, and they let you offer tiers without punishing strangers who share an address. The honest design layers them: a generous per-key limit for normal use, a stricter per-IP limit for unauthenticated endpoints like login, and a global limit that protects the backend even if every key behaves at once. Also limit the right thing: a per-minute count caps requests over time, while a concurrency limit caps simultaneous in-flight requests.

5. Where the limit lives, and why one server is not enough

Enforcement belongs as close to the edge as possible: at the API gateway, the reverse proxy, or a CDN edge, so rejected requests never reach application servers. Application code is the fallback — by the time a request reaches your handler, you have already paid to parse and authenticate it.

One detail decides whether limits work in production: the counter must be shared. An in-memory counter on one server resets on restart and multiplies with scale — five servers each allowing 100 per minute have really allowed 500 per minute. Distributed limiters keep counters in a shared store, typically Redis, using an atomic increment with an expiry for fixed windows, or a sorted set of timestamps for sliding windows. That store is itself a dependency: decide in advance whether the API fails open or fails closed if it goes down, and document the choice.

6. Being a good client: backoff, jitter, and the retry trap

Half of rate limiting is client behaviour, and most client bugs are retry bugs. The pattern that works: on a 429, wait at least Retry-After if present; otherwise back off exponentially — 1 second, then 2, then 4 — with random jitter so throttled clients do not all retry at once. Cap total retries, and give up on requests that are no longer useful.

async function fetchWithBackoff(url, maxRetries = 5) {
  let delay = 1000;
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(url);
    if (res.status !== 429) return res;
    const retryAfter = res.headers.get('Retry-After');
    const wait = retryAfter
      ? Number(retryAfter) * 1000
      : delay + Math.random() * delay;
    await new Promise(r => setTimeout(r, wait));
    delay *= 2;
  }
  throw new Error('Still rate limited after retries');
}

Two more client habits pay off: cache responses where the API allows it, so a repeated read costs zero requests; and read the limit headers on successful responses — when Remaining runs low, slow down before the server has to slow you down.

Diagram of a client sending retry arrows of increasing length to a server, with a clock between them
Backoff with jitter: each retry waits longer than the last, and the random jitter stops a crowd of clients retrying in lockstep.

Common mistakes to avoid

  • Limiting by IP alone on an authenticated API. NAT makes one address many users; use the key or account instead.
  • Returning 403 or 500 instead of 429. Clients cannot back off from a code that does not mean "slow down".
  • Hiding the limit until it is hit. Send limit headers on successful responses too, so clients can pace themselves.
  • Retrying in a tight loop. An immediate retry is just another request against the same quota.
  • Keeping counters per server. Five servers with per-server limits have really set a limit five times higher.
  • Treating rate limiting as security. It slows abuse; it does not authenticate anyone or stop a distributed attack alone.

Done well, rate limiting is almost invisible: legitimate users never notice it, abusive traffic gets a polite but firm no, and the service stays up for everyone else. It sits alongside the status codes that report it, the REST APIs it protects, and the API keys it counts by. Reference: MDN's 429 page (RFC 6585, section 4) and the IETF draft for RateLimit header fields.

Frequently asked questions

What is rate limiting in simple terms?
Rate limiting caps how many requests a client may make in a given time window. An illustrative policy is 100 requests per minute per API key: the 101st request inside that minute is refused, usually with a 429 Too Many Requests response, until the window resets.
What is the difference between rate limiting and throttling?
The terms overlap, but the usual distinction is useful: rate limiting refuses requests beyond a quota, while throttling slows requests down, for example by delaying or queuing them, instead of refusing outright. Many systems do a little of both, and the response headers tell the client which one happened.
What does HTTP 429 mean?
HTTP 429 Too Many Requests means the client has sent too many requests in a given amount of time. It is defined in RFC 6585, section 4. It says nothing about the rest of the service: other clients may be served normally while one client is over its quota.
What is the Retry-After header?
Retry-After tells a client how long to wait before trying again. It carries either a delay in seconds, such as Retry-After: 60, or an HTTP date after which a retry is acceptable. A well-behaved client respects it instead of retrying immediately in a loop.
Should I rate limit by IP address or by API key?
By API key or authenticated user when you can. An IP address is a weaker key because many users can share one public address behind NAT, so one heavy user can get a whole building throttled. IP-based limits still make sense for unauthenticated endpoints such as login, where no key exists yet.
What is a token bucket?
A token bucket is a rate-limiting algorithm. Tokens refill at a fixed rate into a bucket with a maximum size, and each request spends one token. An empty bucket means the request is refused. Because a full bucket holds spare tokens, short bursts are allowed while the long-run average stays at the refill rate.
How should a client handle a 429 response?
Stop, read Retry-After if present, and wait at least that long before retrying. If no Retry-After is given, use exponential backoff with jitter: wait a short interval, then double it on each further 429, adding a random jitter so many clients do not all retry at the same instant. Never retry in a tight loop.
Does rate limiting stop DDoS attacks?
Not on its own. Rate limiting protects an application from overload by individual clients, including buggy loops and abusive scripts. A large distributed attack spreads its requests across many addresses, so it also needs network-level filtering, a CDN or gateway in front of the application, and capacity planning. Rate limiting is one layer, not the whole defence.

Related articles

Try the free tool