Protection

6 min readUpdated

What is rate limiting?

Quick answer

Rate limiting restricts how many requests a given client may make within a period of time, and rejects or delays anything beyond that. It is the simplest effective defence against brute-force login attempts, scraping, API abuse and small floods, because it makes each attempt cost the attacker time regardless of how the request is disguised. The hard parts are choosing what counts as a client and setting a limit that stops abuse without breaking legitimate bursts.

01The common algorithms

Token bucket is the usual choice for public APIs because real clients are bursty: a page load may fire ten requests at once and then nothing for a minute. A strict per-second cap punishes that pattern; a bucket absorbs it while still limiting the sustained rate.

A worked example: a bucket that holds ten tokens and refills at one per second lets a client send ten requests at once, then one a second after that. Ten quiet seconds later it can burst again. nginx's limit_req module uses the closely related leaky-bucket method, in which its burst parameter plays the part of the bucket size.

AlgorithmHow it behavesTrade-off
Fixed windowCounts requests per calendar interval, resets at the boundarySimple, but allows a double burst across the boundary
Sliding windowCounts over the trailing N secondsAccurate, more state to keep
Token bucketTokens refill at a steady rate; each request spends oneAllows short bursts while capping the sustained rate
Leaky bucketRequests drain at a fixed rate; a queue absorbs bursts up to a limitSmooths traffic, but queued requests wait instead of being rejected

02What to count per: the decision that matters most

A rate limit is only as good as its key. Limit per IP address and you punish everyone behind a shared connection: an office, a university, a mobile carrier's NAT, an entire country in some cases. Shared addresses are one of the two usual reasons real visitors hit a limit such as Cloudflare's error 1015; the other is a rule that counts every image and script on a page as a separate request.

IPv6 brings the opposite problem. One subscriber is normally given a whole prefix, a /48 or /56 under the RIPE community's recommendations, so a per-address limit can be dodged by rotating through addresses the client already owns. Key IPv6 limits on a prefix rather than a single address; the /64 is the smallest sensible unit.

  • Per IP: the default; fine for anonymous traffic, unfair to shared connections
  • Per API key or account: much fairer, and possible wherever the client authenticates
  • Per username on login routes: credential stuffing spreads attempts over many addresses, so a per-IP limit alone misses it
  • Per session: works for logged-in users, and forces an attacker to log in before abusing anything
  • Per IP and endpoint: a tight limit on /login with a loose one everywhere else
  • Per subnet or prefix: for abuse spread across many addresses in one network; a subnet calculator shows how many addresses a /24 or a /48 really covers

03Choosing limits per route

The right limit follows from what a route costs you and who calls it: strict and keyed on username and address for login and password reset, per API key for a public API, moderate for search, and usually none for cached static assets. Measure normal traffic first, set the limit comfortably above its busiest legitimate peak, and run the rule in a dry-run mode before enforcing it. A route-by-route table, with what each route should answer when its limit is hit, is in designing limits that spare real users.

04A worked example in nginx

The configuration below puts a strict limit on the login route and a loose one on everything else. limit_req answers with 503 unless you set limit_req_status, which makes a rate limit look like an outage to clients and monitoring alike (see 503 Service Unavailable), and it adds no Retry-After header by itself; what the response should carry is covered below.

With burst=5 and nodelay, the first six login attempts from one address pass at once and later ones are rejected until the bucket drains at five a minute. Before enforcing a new limit, limit_req_dry_run on counts what would have been rejected without rejecting anything. And $binary_remote_addr is only the visitor's address if you have restored the client IP behind a reverse proxy; otherwise every visitor shares the proxy's bucket. Connection limits, timeouts and the inheritance rules that catch people out are covered in nginx DDoS protection.

Per-route limits answering 429 (nginx)
# http {} context
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=site:10m rate=10r/s;
limit_req_status 429;

server {
    listen 80;
    server_name example.com;

    location = /login {
        limit_req zone=login burst=5 nodelay;
        proxy_pass http://127.0.0.1:8080;
    }

    location / {
        limit_req zone=site burst=40 nodelay;
        proxy_pass http://127.0.0.1:8080;
    }
}

05Measuring before you limit

Your access logs already hold the numbers you need. In the common combined log format the client address is the first field and the request path the seventh, so two lines of awk show the heaviest clients on a route and their busiest minute. Run them over a normal week, not over an attack, and set the limit above what real users do.

Finding normal peaks in an access log
# Requests per client to /login
awk '$7 ~ /^\/login/ {print $1}' access.log | sort | uniq -c | sort -rn | head

# Busiest minute per client on /login
awk '$7 ~ /^\/login/ {print $1, substr($4, 2, 17)}' access.log | sort | uniq -c | sort -rn | head

06Responding well when a limit is hit

Answer a per-client limit with 429 Too Many Requests (RFC 6585) and a Retry-After header (RFC 9110), not 503, so callers slow down instead of assuming an outage. Well-written clients honour Retry-After and back off; without it they retry immediately and make the problem worse. In nginx, a named error location adds the header to rejections only, as shown in how site owners should send a 429; the same guide covers how clients should back off when the header is missing.

For public APIs, it is worth returning the limit state on every response rather than only on rejection, so clients can pace themselves before they hit the wall. There is no finished standard yet: the IETF's RateLimit and RateLimit-Policy fields are still an Internet-Draft, and many APIs use their own x-ratelimit-* headers, as GitHub does. Whichever you choose, document it.

A 429 with quota information (IETF draft format)
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 20
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=0;t=20

07What rate limiting cannot do

A rate limit assumes an attacker is constrained by how fast one client can go. A distributed attack removes that assumption: ten thousand machines in a botnet, each making two requests a second, will pass any sane per-client limit while delivering twenty thousand requests a second to your origin.

This is why rate limiting is a layer rather than a solution. It handles brute force, scraping and single-source abuse very well. Distributed floods need DDoS mitigation that reasons about the traffic as a whole.

Questions

Frequently asked questions

There is no universal number: the right limit for a login route and an image route differ by orders of magnitude. Measure your normal traffic per route, set limits comfortably above its legitimate peaks, and test in a dry-run mode before enforcing.

It stops single-source floods and slows brute force. It does not stop a distributed attack, where each individual client stays under the limit and the aggregate still overwhelms you.

Reject with 429 and Retry-After for APIs, so callers can back off deliberately. Delaying holds connections open, which under load turns your rate limiter into its own resource-exhaustion problem.

Because limit_req_status defaults to 503. Set it to 429 so clients, crawlers and monitoring can tell a rate limit from an outage, and add a Retry-After header so well-behaved clients know when to come back.

At minimum, 429 with Retry-After. Many APIs add quota headers to every response, such as GitHub's x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset, or the RateLimit and RateLimit-Policy fields from the current IETF draft.

Sources

Sources and verification

nginx configuration checked with nginx -t and run locally with nginx 1.22.1, confirming six immediate logins followed by 429; awk commands run on a synthetic combined-format log with GNU Awk 5.2, Debian 12, October 2026.

  1. 01RFC 6585: Additional HTTP Status Codes (429 Too Many Requests) · IETF
  2. 02RFC 9110: HTTP Semantics, section 10.2.3 Retry-After · IETF
  3. 03RateLimit header fields for HTTP (Internet-Draft) · IETF
  4. 04Module ngx_http_limit_req_module · nginx.org
  5. 05Rate limits for the REST API · GitHub Docs
  6. 06RIPE-690: IPv6 prefix assignment for end-users · RIPE NCC

Last updated