Rate Limiting in Nginx: limit_req, limit_conn, and burst
Introduction¶
Rate limiting is the cheapest defence against credential stuffing, scrapers,
and accidental client loops. Nginx's built-in limit_req is a leaky bucket —
effective once you understand burst and nodelay, confusing before.
Concepts¶
limit_req— limits the request rate (requests per second/minute) per key (usually client IP).limit_conn— limits concurrent connections per key. Good against slow downloads and connection floods.
Both need a shared-memory zone declared in the http {} block to track
state across worker processes.
The leaky bucket¶
rate=10r/s means one request is allowed every 100 ms. Requests arriving
faster than that are:
- rejected (default), or
- queued up to
burst=Nslots and released at the rate (adds latency), or - served immediately if
burst=N nodelay— up toNexcess requests go through now, then the bucket must drain before more are allowed.
nodelay is almost always what you want for a web app: allow a short spike,
then throttle. Plain burst (no nodelay) is for smoothing a backend that
truly can't handle bursts.
Hands-on¶
Protect a login endpoint¶
http {
# 10 MB zone (~160k IPs), 5 requests/minute per IP
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# General API: 20 r/s, allow a burst of 40
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
# Max 10 concurrent connections per IP
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req_status 429; # not the default 503
limit_conn_status 429;
}
server {
location /api/ {
limit_req zone=api burst=40 nodelay;
limit_conn perip 10;
proxy_pass http://backend;
}
location = /login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://backend;
}
}
$binary_remote_addr (4 or 16 bytes) is used instead of $remote_addr (up to
15 chars) so the zone holds more entries.
Behind a proxy / CDN¶
$binary_remote_addr will be your load balancer's IP — everyone shares one
bucket. Trust the forwarded IP:
set_real_ip_from 10.0.0.0/8; # your LB / CDN ranges
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# now $binary_remote_addr is the real client
Whitelist trusted sources¶
geo $limit {
default 1;
10.0.0.0/8 0; # internal
203.0.113.7/32 0; # partner
}
map $limit $limit_key {
0 ""; # empty key = not rate limited
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=api:10m rate=20r/s;
An empty key disables limiting for that request.
Verification and troubleshooting¶
# Hammer it and watch the status codes
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code} " https://example.com/login; done; echo
# Nginx logs rejections at [error] level
sudo tail -f /var/log/nginx/error.log | grep limiting
# limiting requests, excess: 3.000 by zone "login", client: 1.2.3.4
- Legitimate users getting 429 — the
rateis too low for real usage, or everyone shares one bucket because you're behind a proxy and didn't setreal_ip. Check theclient:IP in the error log — if it's your LB, that's the bug. burstseems ignored — withoutnodelay, burst requests are delayed, not rejected; the client sees slow responses, not 429s. Addnodelay.- Limit not applying at all — the
limit_reqdirective is in alocationthat isn't matching (see location matching), or the zone name is misspelled (Nginx would failnginx -tthough). limit_req_zoneerrors: "zone already bound" — you declared two zones with the same name. Names must be unique acrosslimit_req_zone,limit_conn_zone, etc.- Static assets throttled — your limit is on
location /and catching everything. Scope it to/api/and/login, or exclude static with a separatelocation. - Attack still gets through — a distributed attack from thousands of IPs
defeats per-IP limits. Add
limit_reqon a coarser key ($http_authorizationfor API tokens,$request_urifor one endpoint) or push the problem to a WAF.
Related tools and reading¶
- On-site: HTTP Checker, Security Headers Checker, Blacklist Checker.
- Related posts: An Nginx reverse proxy with TLS, How Nginx location block matching works.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Web & Mail Servers
Running BIND 9 as an Authoritative Server
BIND as an authoritative server: zone files, secured AXFR with TSIG, SOA serial rules, and DNSSEC inline-signing. Here's a working master/slave setup.
October 24, 2026Varnish Cache in Front of a Site
Varnish is a caching HTTP accelerator configured in VCL. Here's the request lifecycle, why you strip cookies, how bans work, and grace mode for serving stale during backend trouble.
October 24, 2026Let's Encrypt with Certbot, Including Renewal That Doesn't Break
Getting the first cert is easy; keeping renewal working for years is the part people get wrong. Webroot vs DNS-01, deploy hooks, and the failure modes.
October 24, 2026