Nginx rate limiting caps how fast a single client can send requests. It stops password brute-forcing, scrapers and small floods before they reach PHP or the database, and it costs almost nothing in CPU. This guide sets up request and connection limits with sensible defaults.
Before you start
- Nginx on a VPS with root access.
- If Nginx sits behind Cloudflare or a load balancer, restore the real client IP first with
set_real_ip_fromandreal_ip_header, or every visitor shares one limit.
How limit_req works
limit_req uses the leaky bucket algorithm. A zone stores a counter per key (usually the client IP) in shared memory. Requests drain at the configured rate; extra requests can wait in a burst queue; anything beyond the burst is rejected with 503 (or the status you choose).
Create a rate limit zone
Zones are defined in the http block, for example in /etc/nginx/conf.d/ratelimit.conf:
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req_status 429;
limit_conn_status 429;$binary_remote_addruses less memory than$remote_addr; 10 MB holds about 160,000 IPs.429 Too Many Requestsis clearer for clients than the default 503.
Apply limits in server and location blocks
server {
limit_req zone=general burst=20 nodelay;
limit_conn perip 20;
location = /wp-login.php {
limit_req zone=login burst=3 nodelay;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}burst and nodelay
- No burst: any request faster than the rate is rejected. Too strict for real pages, which load many assets at once.
- burst=20: up to 20 extra requests queue and are released at the rate, which adds delay.
- burst=20 nodelay: the 20 extra requests are served immediately, but the slots refill only at the rate. This is the best default for websites.
Whitelist trusted IPs
Use geo and map so your office and monitoring IPs get an empty key, which Nginx does not limit:
geo $limited { default 1; 203.0.113.10 0; 10.0.0.0/8 0; }
map $limited $limit_key { 0 ""; 1 $binary_remote_addr; }
limit_req_zone $limit_key zone=general:10m rate=10r/s;Test the limits
- Check and reload:
sudo nginx -t && sudo systemctl reload nginx. - Send a burst from another machine:
for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php; done - You should see 200s, then 429s. Rejections are logged in
error.logas "limiting requests". - Start with
limit_req_dry_run on;(Nginx 1.17.1+) to log would-be rejections without blocking anyone.
Troubleshooting
- Everyone gets 429: all traffic shows the proxy's IP. Configure the real IP module.
- Pages load with missing images: burst too small for asset-heavy pages. Raise burst or exclude static file locations.
- Limits ignored: a more specific location without
limit_reqmatched. Limits inherit from server to location only if the location has none of its own.
FAQ: Nginx rate limiting
Is rate limiting enough against DDoS?
No. It handles small floods and abuse; large attacks must be filtered upstream by a network or CDN service.
What rate suits a login page?
About 5 requests per minute per IP with a small burst stops brute force without bothering humans.
Does LiteSpeed have something similar?
Yes, per-client throttling and anti-DDoS settings in the WebAdmin console.
