Nginx Rate Limiting: limit_req, Bursts and Protecting Your Login Page
How to rate-limit requests in Nginx with limit_req_zone and limit_req: choosing a key and rate, what burst and nodelay really do, returning 429 instead of 503, separate limits for login and API routes, getting the real client IP behind Cloudflare, connection limits, and testing it.
Rate limiting stops one client from sending too many requests: bots brute-forcing your login, scrapers hammering an expensive endpoint, or someone burning through your AI API budget. Nginx can do it before requests ever reach your app. (What Is Rate Limiting?)
The two directives
limit_req_zone (in the http block) defines a limit: what to count by, where to store counters, and the rate.
limit_req (in a server or location) applies it.
http {
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_req_status 429;
server {
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location = /api/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
}
}
}
$binary_remote_addr— limit per client IP (the binary form uses less memory than$remote_addr).zone=login:10m— a named shared-memory zone; 10 MB holds roughly 160,000 IPs.rate=5r/m— five requests per minute. User/sfor per-second.limit_req_status 429— Nginx's default rejection status is 503, which looks like an outage to monitoring and search engines. 429 is the correct "too many requests". (429 Too Many Requests)
burst and nodelay, demystified
Nginx enforces the rate smoothly: 10r/s really means "one request every 100 ms". Without a burst, two requests 50 ms apart — normal for a page loading several resources — would get the second one rejected.
burst=20— allow up to 20 extra requests to queue instead of being rejected.- without
nodelay— queued requests are released at the configured rate, so the client sees them slowed down. - with
nodelay— queued requests are served immediately, but still use up burst slots, which refill at the rate. Excess beyond the burst is rejected.
burst=N nodelay is what most people want: normal users never notice, abusive clients get 429s.
There's also delay=N for a middle ground: the first N excess requests go through immediately and the rest are slowed.
Different limits for different routes
Be strict where abuse is costly, generous elsewhere:
| Route | Suggested starting point |
|---|---|
| Login, signup, password reset | A few per minute per IP |
| Endpoints that call paid AI APIs | Low, per IP — and per user in your app |
| General API | Tens per second with a burst |
| Static assets | Usually none |
(Brute-Force Attacks, Stop Bots Running Up Your AI Bill)
Behind Cloudflare or a load balancer: get the real IP
If Nginx sits behind Cloudflare, every request comes from a Cloudflare IP — so per-IP limiting would throttle everyone together. Use the real-IP module to trust the forwarded header from known proxy ranges:
set_real_ip_from 173.245.48.0/20; # add every Cloudflare range
# ...
real_ip_header CF-Connecting-IP;
Only trust headers from proxy IPs you list; otherwise anyone can fake their IP. (Cloudflare Proxied vs DNS Only, CIDR Notation Explained)
Limiting connections too
limit_conn caps simultaneous connections, useful against slow clients holding connections open:
limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
limit_conn perip 20;
}
Test it
for i in $(seq 1 15); do curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/api/login -X POST; done
You should see 200s (or 401s) followed by 429s. Rejections are logged to the error log. Add limit_req_dry_run on; first to log what would be limited without blocking anyone. (Load Testing Your App)
Nginx limits are a first line, not the only one
IP-based limits don't stop distributed attacks or abuse from logged-in accounts. Add per-user limits in your app (often with Redis), CAPTCHAs on sensitive forms, and account lockouts. (Implement Rate Limiting, Add a CAPTCHA to Your Forms)
The summary
limit_req_zonedefines the limit;limit_reqapplies it per location.- Use
burst=N nodelayso real users aren't affected, andlimit_req_status 429. - Behind a proxy, set the real client IP first — or you'll limit everyone at once.
EasySpawn puts a managed proxy with HTTPS in front of every app, and Claude Code can add per-route and per-user rate limits where your app needs them. See how it works or join the waitlist.
Related: Nginx Reverse Proxy Configuration · fail2ban · What Is a DDoS Attack? · Nginx Location Blocks and try_files
Keep reading
What Is Tailscale? A Private Network for Your Devices and Servers
Tailscale builds a private, encrypted network (a tailnet) between your laptops, phones and servers using WireGuard, with no ports to open. How it works, common uses — private SSH, reaching databases and admin tools, home labs — MagicDNS, ACLs, Funnel and Serve, and alternatives like Headscale and plain WireGuard.
Writing a systemd Service File for Your App (With Hardening)
A production-ready systemd unit for a Node.js or Python app, explained: Type, User, WorkingDirectory, EnvironmentFile, Restart and backoff, graceful stop, resource limits, logging, and the sandboxing options (ProtectSystem, NoNewPrivileges, PrivateTmp) that limit damage if the app is compromised.