I first got serious about rate limiting after watching a client’s login endpoint get hammered by a credential-stuffing bot — tens of thousands of login attempts in under an hour, from a rotating pool of IPs, all aimed at the same /login route. The application itself never crashed, but the database connection pool did, taking the whole site down with it. A few lines of Nginx configuration would have stopped that attack cold before it ever reached the app. Since then, rate limiting has become one of the first things I configure on any public-facing Nginx server, right alongside SSL.
This guide covers everything I use in practice: request rate limiting, connection limiting, burst handling, and how to apply different limits to different parts of an application.
Why Rate Limiting Matters
Rate limiting protects against several distinct problems:
- Brute-force attacks on login forms, password reset endpoints, and API keys
- Scraping and bot abuse that consumes bandwidth and server resources without adding value
- Accidental self-inflicted overload — a buggy client-side retry loop or a misconfigured cron job hammering your own API
- Distributed denial-of-service style abuse — while Nginx rate limiting alone won’t stop a large-scale DDoS, it’s a meaningful first line of defense against smaller-scale abuse
- Resource fairness — ensuring no single client can monopolize server capacity at the expense of everyone else
Requirements
- Nginx installed (rate limiting is built into core Nginx — no extra modules required)
- Root or sudo access
- A general sense of your application’s normal traffic patterns, so you can set limits that block abuse without blocking legitimate users
Step 1: Understanding the Two Rate Limiting Modules
Nginx provides two separate mechanisms:
limit_req— limits the rate of requests (e.g., no more than 10 requests per second per IP)limit_conn— limits the number of concurrent connections (e.g., no more than 5 simultaneous connections per IP)
These solve different problems and are often used together. limit_req is great for controlling request frequency (login attempts, API calls); limit_conn is great for preventing a single client from opening dozens of simultaneous connections (common in download-heavy or streaming scenarios).
Step 2: Setting Up Request Rate Limiting
First, define a rate limiting zone in the http block (in nginx.conf or a file included from it):
http {
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
}
Breaking this down:
$binary_remote_addr— uses the client’s IP address (in compact binary form, which is more memory-efficient than the string form) as the key to track per-client state.zone=general:10m— names this zonegeneraland allocates 10MB of shared memory to track client states. As a rule of thumb, 1MB holds roughly 16,000 IP addresses’ worth of state, so 10MB comfortably handles significant traffic.rate=10r/s— allows 10 requests per second per IP. You can also specifyr/mfor requests per minute if you need finer control at lower rates.
Now apply the zone within a location block:
server {
location /api/ {
limit_req zone=general burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
}
burst=20— allows short bursts of up to 20 requests beyond the steady-state rate, queued and processed as capacity allows. Without a burst allowance, any momentary spike above the exact rate gets rejected immediately, which is often too aggressive for real-world traffic patterns (browsers frequently fire several requests nearly simultaneously for a single page load).nodelay— processes burst requests immediately rather than artificially delaying them to smooth out the rate. Withoutnodelay, Nginx queues burst requests and drips them out at the defined rate, which adds latency. I usenodelayfor most cases since I’d rather reject excess requests outright than slow down legitimate ones.
Step 3: Applying Stricter Limits to Sensitive Endpoints
I always apply tighter limits specifically to login, registration, and password reset endpoints, since these are the most common targets for automated abuse:
http {
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
}
server {
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=general burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
}
Here, /login allows only 1 request per second with a burst of 5, while general API traffic gets a much more generous 10 requests per second. This kind of tiered approach — strict on high-risk endpoints, relaxed on normal browsing/API traffic — is the pattern I use on nearly every production site.
Step 4: Limiting Concurrent Connections
For scenarios like file downloads or streaming, where a single request might be long-lived, request-rate limiting alone doesn’t help. This is where limit_conn comes in:
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
}
server {
location /downloads/ {
limit_conn addr 3;
proxy_pass http://127.0.0.1:3000;
}
}
This restricts each client IP to at most 3 simultaneous connections to /downloads/. This is particularly effective against download managers or scripts trying to grab many files in parallel to bypass bandwidth throttling.
Step 5: Custom Error Responses for Rate-Limited Requests
By default, rate-limited requests get a bare 503 Service Temporarily Unavailable. I prefer customizing this to be more informative and to use the more semantically correct 429 Too Many Requests status code:
server {
limit_req_status 429;
limit_conn_status 429;
location /api/ {
limit_req zone=general burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
error_page 429 /429.json;
location = /429.json {
internal;
default_type application/json;
return 429 '{"error": "Too many requests. Please slow down and try again shortly."}';
}
}
This gives API clients a proper JSON error response with the correct status code, which well-behaved clients can use to implement backoff logic automatically.
Step 6: Rate Limiting Based on Other Keys
While IP-based limiting is the most common approach, you can key rate limits on other request attributes. For an authenticated API, limiting by API key rather than IP is often more accurate, since many legitimate users can share an IP (corporate NAT, mobile carrier NAT):
http {
limit_req_zone $http_x_api_key zone=api_key_limit:10m rate=100r/s;
}
server {
location /api/ {
limit_req zone=api_key_limit burst=50 nodelay;
proxy_pass http://127.0.0.1:3000;
}
}
You can also combine multiple keys using a map directive for more sophisticated logic — for instance, exempting known internal IPs from rate limiting entirely:
geo $limit_bypass {
default 0;
10.0.0.0/8 1;
203.0.113.5 1;
}
map $limit_bypass $limit_key {
0 $binary_remote_addr;
1 "";
}
limit_req_zone $limit_key zone=general:10m rate=10r/s;
When $limit_key evaluates to an empty string, Nginx effectively doesn’t apply the rate limit for that request, since all such requests share a single (unlimited-in-practice) empty key — this is the standard technique for whitelisting internal traffic.
Step 7: Whitelisting Specific IPs Entirely
For monitoring services, internal health checks, or trusted partners, sometimes it’s simpler to bypass rate limiting entirely with geo combined with the map pattern above, or by placing exempted routes in a separate location block without the limit_req directive at all:
location /health {
# No limit_req applied here
proxy_pass http://127.0.0.1:3000;
}
Testing Rate Limits
I test rate limiting with a simple loop using curl:
for i in {1..30}; do
curl -o /dev/null -s -w "%{http_code}\n" http://example.com/api/test
done
You should see a mix of 200 responses up to your configured rate/burst, followed by 429 (or 503) responses once the limit is exceeded.
For more realistic load testing, I use ab (Apache Bench) or wrk:
ab -n 100 -c 20 http://example.com/api/test
Check your Nginx error log during testing — rate-limited requests are logged there by default:
tail -f /var/log/nginx/error.log | grep limiting
You’ll see entries like:
2026/08/15 11:02:14 [error] 1523#1523: *201 limiting requests, excess: 10.500 by zone "general", client: 203.0.113.45
Troubleshooting Common Issues
Legitimate users getting rate limited — Your rate or burst values are too aggressive for real usage patterns. Browsers commonly fire 5-10+ requests simultaneously when loading a page (HTML, CSS, JS, images, fonts, API calls). Increase burst and/or add nodelay, or raise the base rate.
Rate limiting not applying at all — Double check the zone is actually referenced inside a location block with limit_req zone=yourzone; — defining the zone in http alone does nothing without an explicit limit_req directive applying it somewhere.
All users behind a corporate NAT getting blocked together — This is the classic downside of IP-based limiting. Consider keying by a more granular identifier (API key, session cookie) if this becomes a real problem for your user base.
Rate limits reset unexpectedly after Nginx reload — This is actually expected behavior: limit_req_zone state lives in shared memory that gets reinitialized when Nginx restarts (though a reload alone, without a full restart, generally preserves it since worker processes are recycled gracefully — test this specifically in your environment if it matters for your use case).
Security Considerations
- Rate limiting is not a substitute for proper application-level security. Always pair with additional protections like account lockout policies, CAPTCHA on suspicious activity, and multi-factor authentication for sensitive actions.
- IP-based limiting can be evaded by distributed attacks using large IP pools (botnets, residential proxy networks). For serious threats, layer Nginx rate limiting with a dedicated WAF or DDoS mitigation service (Cloudflare, AWS Shield, etc.) in front of your server.
- Don’t rate limit based on spoofable headers like
X-Forwarded-Forunless you’ve properly configuredset_real_ip_fromto only trust that header from your actual upstream proxies. - Log and monitor rate limit triggers — a sudden spike in
429/503responses is itself a useful signal of an ongoing attack, even if the rate limiting is successfully mitigating it.
Performance Tips
- Rate limiting has minimal CPU overhead — the shared memory zone lookups are fast — but size your zones appropriately for expected unique client counts to avoid memory pressure under high load.
- Use
nodelayfor latency-sensitive endpoints (APIs) rather than letting Nginx queue and delay burst requests, unless smoothing traffic is specifically what you want (e.g., protecting a rate-limited third-party API you’re proxying to). - Combine
limit_reqandlimit_connjudiciously — applying both to every single location adds a small amount of overhead per request; reserve stricter combined limiting for genuinely sensitive endpoints rather than blanket-applying to your entire site.
Real-World Use Cases
- Protecting login/authentication endpoints from brute-force and credential-stuffing attacks — the scenario that got me into rate limiting in the first place.
- API rate limiting for third-party consumers, enforcing fair usage tiers before requests even reach your application logic.
- Preventing scraper abuse on content-heavy sites, where bots hammering every page can degrade performance for real visitors.
- Protecting expensive backend operations — search endpoints, report generation, or anything computationally heavy that shouldn’t be callable in rapid succession.
- Mitigating small-to-medium scale DDoS attempts as a first line of defense before traffic reaches your application servers.
Best Practices I Follow
- Apply stricter limits to authentication and other sensitive endpoints than to general traffic.
- Always use
burstto accommodate legitimate traffic spikes — a zero-burst config is almost always too aggressive. - Return
429 Too Many Requestswith a clear, machine-readable error body rather than a bare503. - Whitelist trusted internal IPs and health-check endpoints explicitly.
- Test rate limits under realistic traffic patterns before deploying to production, not just synthetic single-request loops.
- Monitor error logs for rate limit triggers as an early warning signal of abuse.
- Layer Nginx rate limiting with application-level protections (account lockouts, CAPTCHA) rather than relying on it alone.
- Reassess rate limit thresholds periodically as your traffic patterns evolve.
Wrapping Up
Rate limiting in Nginx is inexpensive to set up and genuinely effective against a wide range of common abuse patterns — brute-force attempts, scraper abuse, accidental self-inflicted overload, and smaller-scale denial-of-service attempts. The configuration itself is only a few lines, but getting the thresholds right takes a bit of observation of your actual traffic. I’d suggest starting conservative on sensitive endpoints like login forms, monitoring your error logs for a week to see how real users interact with your limits, and adjusting from there. It’s a small amount of upfront work that can prevent a genuinely bad night dealing with an active attack.