How to Implement Nginx Rate Limiting: Protect Against Brute‑Force, Crawlers, and Traffic Spikes
This article explains how to use Nginx's built‑in limit_req and limit_conn modules—based on the leaky‑bucket algorithm—to throttle requests by IP or API key, configure burst and nodelay behavior, apply whitelists, customize error responses, fine‑tune with delay and dry‑run modes, and monitor effectiveness in production environments.
Problem Background
Production environments often need Nginx rate limiting to mitigate malicious request bursts such as credential‑brute‑force attacks, aggressive crawlers, sudden traffic spikes from promotions, and individual users exceeding reasonable request rates.
Core Principle: Leaky Bucket Algorithm
The limit_req module implements a leaky‑bucket algorithm where rate defines the steady request flow, burst defines the buffer size for sudden spikes, and nodelay/delay control whether excess requests are queued or rejected. Compared with a token‑bucket, the leaky bucket enforces a constant output rate, making it suitable for protecting back‑ends from burst overload.
Configuration Directives Overview
limit_req_zone– scope: http – defines a shared memory zone and request rate. limit_req – scope: http/server/location – enables rate limiting in a specific context. limit_req_status – scope: http/server/location – custom response code (default 503, recommended 429). limit_req_log_level – scope: http/server/location – log level for rejected or delayed requests. limit_req_dry_run – scope: http/server/location – 1.17.1+ dry‑run mode (statistics only, no enforcement).
limit_req_zone Parameters
http {
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s;
# key (limit basis) zone name:size rate
} $binary_remote_addrstores the client IP in binary form (4 bytes IPv4, 16 bytes IPv6) and saves memory compared with $remote_addr. zone=name:size defines a shared memory area; on a 64‑bit system each client state occupies ~128 bytes, so 10 MB can hold ~80 000 clients. rate=10r/s limits to 10 requests per second; supports r/s and r/m (e.g., 30r/m for 30 requests per minute).
limit_req Parameters
location /api/ {
limit_req zone=per_ip burst=20 nodelay;
# zone burst queue size
} zoneselects the defined limit zone. burst sets the maximum number of excess requests that can be queued. nodelay lets excess requests pass immediately; without it, excess requests are delayed, increasing response time. delay=N (1.15.7+) delays only after the first N excess requests.
Basic Configuration Examples
IP‑Based Limiting (most common)
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
listen 80;
server_name api.example.com;
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
}Effect: each IP can send up to 10 r/s, a burst of 20 is processed instantly, and requests beyond rate+burst receive 429. Without nodelay, excess requests are queued and response latency grows.
API‑Key Limiting
http {
limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/m;
server {
location /v1/ {
if ($http_x_api_key = "") { return 401; }
limit_req zone=per_key burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
}Uses the X-API-Key request header (available as $http_x_api_key) as the limiting key, suitable for multi‑tenant APIs.
Combined IP + Key Limiting
http {
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=5r/s;
limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/m;
server {
location /v1/ {
limit_req zone=per_ip burst=10 nodelay;
limit_req zone=per_key burst=20 nodelay;
proxy_pass http://backend;
}
}
}Both directives are AND‑related; a request must satisfy both IP and API‑key limits.
Advanced Scenarios
Fine‑Grained delay Control (1.15.7+)
location /api/ {
limit_req zone=per_ip burst=12 delay=8;
# first 8 excess requests pass, later ones are delayed
}Behavior:
≤ 10 requests in a window – all processed normally.
11‑18 requests – first 10 normal, next up to 8 pass instantly.
19‑22 requests – first 10 normal, next 8 pass, remaining delayed.
> 22 requests – first 10 normal, next 8 pass, then delayed, excess rejected (429).
Whitelist Bypass (geo + map)
http {
geo $limit {
default 1;
127.0.0.1 0;
10.0.0.0/8 0;
172.16.0.0/12 0;
192.168.0.0/16 0;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=whitelist:10m rate=10r/s;
server {
location /api/ {
limit_req zone=whitelist burst=20 nodelay;
proxy_pass http://backend;
}
}
}If $limit_key is empty (whitelisted IP), no state is created and the request bypasses rate limiting.
Custom Error Responses
Default limit‑req returns 503, which may be retried by browsers. Returning 429 with a JSON body is recommended.
http {
limit_req_status 429;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
error_page 429 = @rate_limit;
location @rate_limit {
internal;
default_type application/json;
add_header Retry-After 5;
return 429 '{"code":429,"message":"Too many requests, please retry later","retry_after":5}';
}
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
}
}Using an internal redirect adds a small overhead; for ultra‑low latency a static JSON file or variable‑based template can be used.
URI‑Based Differential Limiting
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=static:10m rate=100r/s;
server {
location /api/login/ { limit_req zone=login burst=5 nodelay; limit_req_status 429; proxy_pass http://backend; }
location /api/ { limit_req zone=api burst=30 nodelay; limit_req_status 429; proxy_pass http://backend; }
location /static/ { limit_req zone=static burst=200 nodelay; limit_req_status 429; root /var/www/static; }
}
}Dry‑Run Mode (1.17.1+)
http {
limit_req_zone $binary_remote_addr zone=dry:10m rate=10r/s;
server {
location /api/ {
limit_req_dry_run on;
limit_req zone=dry burst=20 nodelay;
proxy_pass http://backend;
}
}
}In dry‑run, Nginx logs which requests would have been limited without rejecting them. The $limit_req_status variable (1.17.6+) reports statuses such as PASSED, DELAYED, REJECTED, and their dry‑run equivalents.
Connection‑Count Limiting (limit_conn)
http {
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
location /api/ {
limit_conn conn_per_ip 10;
limit_conn_status 429;
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
}
} limit_conncaps concurrent connections per IP, useful for download throttling or WebSocket limits.
Configuration Validation & Testing
Syntax Check
$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulLoad Test with ab
# 150 requests, 20 concurrency
$ ab -n 150 -c 20 http://api.example.com/api/
Complete requests: 150
Failed requests: 30 # limited (429)
Requests per second: 45.50 [#/sec] (mean)Log Inspection
$ tail -f /var/log/nginx/error.log | grep "limiting requests"
2025/08/15 15:00:22 [warn] ... limiting requests, excess: 5.0 by zone "per_ip", client: 1.2.3.4, ...
$ grep "429" /var/log/nginx/access.log | awk '{print $1,$4,$7,$9}' | tail -10The excess field shows how many requests exceeded the rate.
Monitoring
In open‑source Nginx, monitor the count of 429 responses in access logs; Nginx Plus or ngx_http_stub_status_module can expose metrics.
$ tail -10000 /var/log/nginx/access.log | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -rnMemory Estimation
Each client state ≈ 128 bytes on 64‑bit systems:
1 MB ≈ 8 000 clients
10 MB ≈ 80 000 clients
100 MB ≈ 800 000 clientsTypical sizing: 10 MB for ~100 k daily active users; 50‑100 MB for high‑traffic public services.
Limitations & Alternatives
State not shared across nodes – each Nginx instance tracks limits locally, causing inaccurate cluster‑wide limits. Alternative: Redis + Lua or a centralized rate‑limit gateway.
Statistics reset on restart – shared‑memory zones are cleared when Nginx restarts. Alternative: No built‑in persistence; external storage required.
Cannot limit by time window – native modules lack sliding‑window “N per hour” support. Alternative: Custom Lua scripts.
Granularity limited to IP, header, URL variables – more complex keys need extra processing. Alternative: OpenResty + Lua for custom key generation.
Production Deployment Checklist
Ensure 429 status is monitored before enabling limits.
Start with limit_req_dry_run for at least one business cycle.
Set thresholds with headroom (e.g., business peak 1000 QPS → limit at 1200 QPS with burst).
Obtain rate limits from backend owners; verify typical user request counts.
Return user‑friendly JSON messages for critical APIs instead of raw 429 pages.
Combine rate limiting with degradation strategies (WAF, CDN, DDoS protection) for extreme attacks.
Conclusion
The three essential elements of Nginx rate limiting are:
1. limit_req_zone (define rule) → 2. limit_req (apply rule) → 3. limit_req_status (custom reject code)Recommended configurations per scenario:
Login endpoint – rate=2r/s burst=5 nodelay General API – rate=20r/s burst=30 nodelay Multi‑tenant API – key‑based zone with per‑key rate.
Internal systems – whitelist IPs to skip limiting.
Flash sales – temporarily lower rate, use error_page to show a queue page.
Best practices distilled from the analysis:
Use 429 for rate‑limit rejections; it is easy to monitor.
Prefer delay=N to allow limited bursts without immediate rejection.
Store IPs as $binary_remote_addr to reduce memory usage.
Employ geo + map for whitelist bypass.
Validate new policies in dry‑run mode before production rollout.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
