A login form, search endpoint, or API route can overload a backend long before the rest of the site looks busy. Nginx rate limiting caps request frequency at the edge, so abusive clients slow down before they reach PHP-FPM, an application API, or another upstream service.
A practical Nginx rate limiting setup needs a shared memory zone with limit_req_zone, an applied policy with limit_req, and a test/reload workflow that catches syntax mistakes before production traffic sees them. The same configuration can tune bursts, log decisions with limit_req_log_level, dry-run limits, record $limit_req_status, and preserve real client IPs behind a reverse proxy.
Understand Nginx Rate Limiting Directives
Nginx implements rate limiting with the ngx_http_limit_req_module module and its “leaky bucket” method. Requests enter the bucket at the configured rate, and excess requests are delayed, passed through a burst allowance, or rejected with an error response. The official Nginx rate limiting module documentation is the source for directive context, defaults, and version-introduced notes.
Before You Configure Nginx Rate Limiting
Start with Nginx installed and running on the server you plan to protect. If you still need package setup, use the appropriate distribution guide:
You also need root or sudo access to edit Nginx configuration files, usually under /etc/nginx/. Test syntax before every reload:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Core Nginx Rate Limiting Directives and Variables
| Directive or variable | Context | Default or version note | Purpose |
|---|---|---|---|
limit_req_zone | http | No default; requires a key, zone, and rate. | Defines a shared memory zone to store rate limiting state and sets the request rate. |
limit_req | http, server, location | No default; inherits only when no limit_req exists at the current level. | Applies the rate limit with optional burst, delay, or nodelay behavior. |
limit_req_status | http, server, location | Default 503; appeared in Nginx 1.3.15. | Sets the HTTP status code for rejected requests. |
limit_req_log_level | http, server, location | Default error; appeared in Nginx 0.8.18. | Sets the log level for rejected requests; delayed requests log one level lower. |
limit_req_dry_run | http, server, location | Default off; appeared in Nginx 1.17.1. | Counts excessive requests without delaying or rejecting them. |
$limit_req_status | embedded variable | Appeared in Nginx 1.17.6. | Records whether a request passed, was delayed, was rejected, or matched dry-run behavior. |
How limit_req_zone Works
The limit_req_zone directive creates a shared memory zone that tracks request rates. It must be placed in the http context, typically in your main nginx.conf file before any server blocks. The syntax is:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
This directive has three parts:
- Key (
$binary_remote_addr): The variable used to identify clients. Using$binary_remote_addrinstead of$remote_addrsaves memory because it stores IP addresses in binary format (4 bytes for IPv4, 16 bytes for IPv6). Requests with an empty key are not counted, which is useful for allowlist maps. - Zone (
zone=mylimit:10m): Creates a 10 megabyte shared memory zone named “mylimit”. One megabyte can store approximately 16,000 states on 32-bit platforms or 8,000 states on 64-bit platforms. - Rate (
rate=5r/s): Limits requests to 5 per second. For rates below 1 per second, use requests per minute (for example,rate=30r/mfor one request every two seconds).
How limit_req Works
The limit_req directive enables rate limiting in a specific context. It references a zone created by limit_req_zone and optionally configures burst handling:
limit_req zone=mylimit burst=10 nodelay;
The parameters control how excess requests are handled:
- zone: References the shared memory zone defined by
limit_req_zone. - burst: Allows this many requests to queue beyond the rate limit. Without burst, any request exceeding the rate is immediately rejected. With
burst=10, up to 10 excess requests wait in a queue. - nodelay: Processes queued burst requests immediately instead of spacing them according to the rate. This provides a better user experience for legitimate traffic spikes while still enforcing the overall limit.
- delay=N (Nginx 1.15.7+): Sets the excess-request point where delaying starts. For example,
burst=20 delay=10lets the first 10 excessive requests pass without delay, delays the remaining burst requests, and rejects requests beyond the burst allowance.
Configure Basic Nginx Rate Limiting
This baseline Nginx rate limiting configuration caps each client IP at 2 requests per second with a burst allowance of 5 requests. It uses 429 Too Many Requests for rejected traffic, which usually describes rate limiting more clearly than the module’s default 503.
Server-Wide Rate Limit
Add the limit_req_zone directive to your http block in /etc/nginx/nginx.conf:
http {
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
# Keep existing http-level settings here.
}
Then apply the limit in your server block or a specific location. Create or edit your site configuration file (for example, /etc/nginx/sites-available/example.com or /etc/nginx/conf.d/example.conf):
server {
listen 80;
server_name example.com;
location / {
limit_req zone=mylimit burst=5 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
This configuration provides:
- Base rate: 2 requests per second per client IP
- Burst tolerance: Up to 5 additional requests can be processed immediately
- No delay: Burst requests are served instantly rather than queued
- 429 status: Rejected requests receive “429 Too Many Requests” instead of the default 503
Test and Apply the Configuration
After saving your configuration, test the syntax and reload Nginx:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx only after the syntax test succeeds:
sudo systemctl reload nginx
If the syntax test fails, Nginx reports the file and line number containing the error. Common mistakes include missing semicolons, mismatched braces, or placing limit_req_zone outside the http context.
Practical Nginx Rate Limiting Examples
After adapting any example below, run sudo nginx -t before reloading Nginx. The examples show the rate-limiting directives in context, but your production file may also need existing TLS, proxy, logging, or include settings preserved.
Different Rates for Different Locations
In many applications, some endpoints require stricter limits than others. Login pages and API endpoints often need tighter controls, while static assets can be more permissive. You can define multiple zones with different rates:
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
server {
listen 80;
server_name example.com;
location /login {
limit_req zone=login burst=3;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
location / {
limit_req zone=general burst=50 nodelay;
proxy_pass http://127.0.0.1:3000;
}
}
}
This configuration applies progressively stricter limits:
- Login page: 1 request per second (burst 3) to prevent brute-force attacks
- API endpoints: 10 requests per second (burst 20) to balance usability with protection
- General traffic: 30 requests per second (burst 50) for normal page loads
Multiple Rate Limits on the Same Location
You can apply multiple limit_req directives to enforce both per-IP and server-wide limits simultaneously. This prevents a single client from consuming too many resources while also protecting against distributed attacks:
http {
limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
limit_req_zone $server_name zone=perserver:10m rate=100r/s;
server {
listen 80;
server_name example.com;
location / {
limit_req zone=perip burst=10 nodelay;
limit_req zone=perserver burst=50;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
}
With this configuration, each client can make 5 requests per second, but the total requests to the server (across all clients) cannot exceed 100 per second. A request must pass both limits to be processed.
Allowlisting Trusted IP Addresses
Internal services, monitoring systems, and trusted partners often need unrestricted access. Use the geo directive to create a variable that identifies trusted networks, then apply rate limiting conditionally:
http {
geo $limit {
default 1;
10.0.0.0/8 0;
192.168.0.0/16 0;
172.16.0.0/12 0;
127.0.0.1 0;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=mylimit:10m rate=5r/s;
server {
listen 80;
server_name example.com;
location / {
limit_req zone=mylimit burst=10 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
}
This approach works by:
- The
geoblock sets$limitto 0 for trusted networks and 1 for everyone else - The
mapblock converts this into a key: empty string for trusted IPs, client IP for others - Requests with an empty key are not tracked by the rate limiter, effectively bypassing it
Add your actual internal network ranges to the
geoblock. The example uses RFC 1918 private address ranges. If your monitoring or CI/CD systems use specific IPs, include those as well.
Rate Limit Noisy User Agents
Some bulk fetchers can be rate limited more aggressively than regular traffic. Use the map directive to create a key only for user agents you want to throttle:
http {
map $http_user_agent $bot_limit_key {
default "";
~*(curl|wget|python-requests|scrapy|httpclient) $binary_remote_addr;
}
limit_req_zone $bot_limit_key zone=botlimit:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=userlimit:10m rate=10r/s;
server {
listen 80;
server_name example.com;
location / {
limit_req zone=botlimit burst=5;
limit_req zone=userlimit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
}
This configuration applies two overlapping limits: a strict 1 request per second limit for selected bulk user agents, plus a more permissive 10 requests per second limit for all traffic. Regular users are effectively limited to 10 requests per second, while matching bulk clients are limited to 1 request per second.
User-Agent strings are easily spoofed, so this approach is not a security control. Avoid throttling major search crawlers by name unless logs prove they are causing a real load problem; use verified crawler controls and search console tooling before restricting discovery traffic.
Log Nginx Rate Limiting Decisions
Logging shows whether Nginx is rejecting traffic, delaying bursts, or simply counting excessive requests in dry-run mode. Use the error log for immediate troubleshooting and the access log when you need request-by-request reporting.
Set Nginx limit_req_log_level
The Nginx limit_req_log_level directive defaults to error and appeared in version 0.8.18, as listed in the official Nginx limit_req_log_level documentation. Rejected requests use the configured level, while delayed requests are logged one level lower.
http {
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
limit_req_log_level warn;
server {
location / {
limit_req zone=mylimit burst=10;
# Inherits limit_req_log_level from http context
}
}
}
The available log levels and their behavior:
- error (default): Logs rejected requests at
errorand delayed requests atwarn - warn: Logs rejected requests at warn level, delayed requests at notice level
- notice: Logs rejected requests at notice level, delayed requests at info level
- info: Logs rejected requests at info level, delayed requests at debug level
Read Nginx Rate Limit Log Entries
When rate limiting activates, Nginx writes entries to the error log. Use tail in Linux with grep in Linux to filter the log while testing:
sudo tail -f /var/log/nginx/error.log | grep limiting
Rate limit log entries look like this:
<timestamp> [error] <pid>#<tid>: *<connection> limiting requests, excess: 5.432 by zone "mylimit", client: 192.0.2.1, server: example.com, request: "GET /api/data HTTP/1.1", host: "example.com"
The “excess” value shows how far over the limit the request was. Use this information to tune your rate and burst settings.
Log $limit_req_status in Nginx Access Logs
The Nginx $limit_req_status variable appeared in version 1.17.6, as listed in the official Nginx embedded variable documentation. It records PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN, or REJECTED_DRY_RUN, which makes access logs easier to aggregate than free-form error-log messages.
http {
log_format rate_limit '$remote_addr "$request" status=$status limit_req=$limit_req_status';
access_log /var/log/nginx/access.log rate_limit;
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
server {
location / {
limit_req zone=mylimit burst=10;
limit_req_status 429;
}
}
}
A rejected request then appears with a structured rate limit result:
192.0.2.1 "GET /api/data HTTP/1.1" status=429 limit_req=REJECTED
Use Nginx limit_req_dry_run for Testing
The Nginx limit_req_dry_run directive appeared in version 1.17.1 and defaults to off. Turn it on to count excessive requests without delaying or rejecting them while you tune a new limit:
http {
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
server {
location / {
limit_req zone=mylimit burst=10;
limit_req_dry_run on;
limit_req_log_level notice;
}
}
}
In dry run mode, the log still shows which requests would be limited, but all requests are processed normally. Pair it with $limit_req_status in the access log to distinguish DELAYED_DRY_RUN and REJECTED_DRY_RUN from enforced limits, then disable dry run when the numbers look safe.
Test Nginx Rate Limit Behavior
After configuring Nginx rate limits, test both syntax and request behavior. Quick checks with curl in Linux are enough for a first pass, while Apache Bench can generate heavier request bursts.
Quick Nginx Rate Limit Test with curl
Use a shell loop to send rapid requests and observe rate limiting in action. Replace http://example.com/ with the host or path where you applied the limit:
TARGET="http://example.com/"
for i in {1..15}; do curl -s -o /dev/null -w "Request $i: %{http_code}\n" "$TARGET"; done
With a rate limit of 2 requests per second and burst of 5, you should see output similar to:
Request 1: 200 Request 2: 200 Request 3: 200 Request 4: 200 Request 5: 200 Request 6: 200 Request 7: 200 Request 8: 429 Request 9: 429 Request 10: 429 Request 11: 429 Request 12: 429 Request 13: 429 Request 14: 429 Request 15: 429
The first several requests succeed because the burst allowance (5) plus the initial rate allowance (approximately 2) allows rapid requests through before limiting kicks in. Subsequent requests receive 429 responses. If you configured limit_req_status 503 or left it at the default, you would see 503 instead of 429. The exact number of successful requests depends on timing, as the rate limiter continuously refills the bucket.
Load Test Nginx Rate Limits with Apache Bench
For more comprehensive testing, use Apache Bench (ab) to simulate concurrent connections. Install it first if needed.
Run load tests against a staging hostname or a maintenance window first. Apache Bench can create enough traffic to trigger real application errors, upstream saturation, or automated blocking rules.
Debian and Ubuntu:
sudo apt install apache2-utils
Fedora, RHEL, and AlmaLinux:
sudo dnf install httpd-tools
Then run a test with 100 requests at concurrency 10:
ab -n 100 -c 10 http://example.com/
Look for these key metrics in the output:
Concurrency Level: 10 Time taken for tests: [varies] Complete requests: 100 Failed requests: [may include Length differences] Non-2xx responses: [rate-limited responses] Requests per second: [varies] [#/sec] (mean)
Use Non-2xx responses as the main signal that Nginx returned rate-limit responses such as 429 or 503. Apache Bench can also count different response body lengths as failed requests, so do not treat the Failed requests line as a network-failure count by itself.
Advanced Nginx Rate Limiting Considerations
Rate Limiting Behind a Reverse Proxy
When Nginx sits behind a load balancer or CDN, $binary_remote_addr shows the proxy’s IP unless the real IP module rewrites the client address first. Nginx reverse proxy rate limiting should key limits on the restored client IP, not the load balancer address, and should trust only controlled proxy addresses before using headers such as X-Forwarded-For.
Configure Nginx to trust the real IP header from your proxy or CDN, using the actual proxy address range instead of a catch-all network:
http {
# Trust proxy at 10.0.0.1 to send correct X-Forwarded-For
set_real_ip_from 10.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
# Keep other http-level settings here.
}
With this configuration, $binary_remote_addr reflects the real client IP extracted from the X-Forwarded-For header. Source-built Nginx needs the real IP module enabled at build time, while most distribution packages include it. Check the official Nginx real IP module documentation for directive details, and see the Nginx reverse proxy guide for proxy header and upstream examples.
Zone Sizing and Memory Usage
Each rate limit zone consumes shared memory. The formula depends on your platform:
- 32-bit platforms: Each state uses 64 bytes (approximately 16,000 IPs per megabyte)
- 64-bit platforms: Each state uses 128 bytes (approximately 8,000 IPs per megabyte)
For most applications, 10m (10 megabytes) is sufficient. If you serve millions of unique IPs and see “zone storage exhausted” errors in your logs, increase the zone size:
limit_req_zone $binary_remote_addr zone=mylimit:50m rate=5r/s;
When a zone becomes full, Nginx tries to remove stale entries to make room for a new state. If it still cannot create that state, the request is rejected with the configured limit_req_status. A zone that constantly evicts entries or rejects requests for storage pressure is too small for the traffic pattern.
Distributed Rate Limiting
The standard Nginx rate limiting module stores state in memory, which is not shared across multiple servers. If you run multiple Nginx instances behind a load balancer, each tracks limits independently. A client could exceed the intended rate by distributing requests across servers.
For consistent rate limiting across a cluster, you have several options:
- Session persistence: Configure your load balancer to route all requests from a client to the same Nginx instance
- Edge rate limiting: Apply rate limits at your CDN or load balancer instead of individual Nginx servers
- External solutions: Use Nginx Plus (commercial) with zone sync, or implement application-level rate limiting with Redis
Third-party Lua modules like
lua-resty-limit-trafficcan provide Redis-backed rate limiting for clustered deployments. They require the OpenResty distribution or a custom Nginx build with Lua support, so treat them as a separate architecture choice rather than a drop-in setting.
Combine Nginx Rate Limiting with Automated Blocking
Rate limiting slows abusive clients, but persistent offenders may need a temporary firewall block. Tools like Install Fail2ban on Debian or Install Fail2ban on Ubuntu can monitor Nginx logs for rate limit events and block repeat offenders at the firewall level.
Fail2ban watches log files for patterns such as repeated 429 responses and adds firewall rules for the offending IP. Keep the ban rule stricter than the rate limit itself so a brief burst from a shared office, VPN, or mobile network does not become an automatic block.
Troubleshoot Nginx Rate Limiting Issues
Rate Limit Not Activating
If requests are never being limited, check these common causes:
Zone not defined in http context
The limit_req_zone directive must appear in the http block, not inside a server or location block. Check your configuration with line numbers:
sudo grep -R -n "limit_req_zone" /etc/nginx/
If it appears inside a server block, move it to the main nginx.conf http block.
Zone name mismatch
The zone name in limit_req must exactly match the name in limit_req_zone. A typo like zone=mylimit versus zone=my_limit causes the configuration test to fail.
Configuration not reloaded
After editing configuration, you must reload Nginx:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Then reload the service:
sudo systemctl reload nginx
Legitimate Users Being Rate Limited
If real users report being blocked, check these potential issues:
Rate too restrictive
A single page load often requires many requests (HTML, CSS, JavaScript, images). A limit of 5 requests per second may trigger on normal browsing. Check your access logs to understand typical request patterns:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
This shows the top 10 IPs by request count. If legitimate users make 50+ requests loading a page, you need a higher burst value.
Shared IP addresses
Users behind corporate NAT, mobile carriers, or VPNs share IP addresses. Rate limits per IP effectively limit all users from that network. Consider using a different key (like a session cookie or API key) for authenticated endpoints.
Missing burst or nodelay
Without the burst parameter, any request exceeding the rate is immediately rejected. Add a reasonable burst value to accommodate normal traffic patterns. Without nodelay, burst requests are queued and served slowly, which can make pages feel sluggish.
Configuration Syntax Errors
Nginx reports configuration errors when you run nginx -t. Relevant output for a misplaced zone directive includes:
<timestamp> [emerg] <pid>#<tid>: "limit_req_zone" directive is not allowed here in /etc/nginx/sites-enabled/example:7 nginx: configuration file /etc/nginx/nginx.conf test failed
This error means the directive is in the wrong context. limit_req_zone belongs in the http block.
<timestamp> [emerg] <pid>#<tid>: zero size shared memory zone "mylimit" nginx: configuration file /etc/nginx/nginx.conf test failed
The zone name referenced by limit_req does not exist. Check spelling and ensure limit_req_zone is loaded before the server block that uses it.
Conclusion
Nginx rate limiting is ready when zones, burst rules, logging, and reload checks match real traffic instead of a guessed request count. Start conservatively, watch $limit_req_status and error-log entries, then tighten noisy endpoints such as login and API routes. For adjacent hardening, add Nginx security headers or Nginx gzip compression where they fit your site.


Hello,
Below example does not work..! adding request rate as a variable is not supported by nginx..!
Example:
http {
map $http_x_traffic $dynamic_rate {
default “5r/s”;
high “2r/s”;
}
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=$dynamic_rate;
server {
listen 80;
server_name example.com;
location / {
limit_req zone=mylimit burst=10;
proxy_pass http://backend;
}
}
}
Thanks for the follow-up, Sagar. You were correct again. The
rateparameter inlimit_req_zonerequires a constant value. Nginx does not support variables there, so therate=$dynamic_rateexample you saw would fail at configuration load.The article has been rewritten and that section removed entirely. For dynamic rate limiting in Nginx, you need either multiple zones with conditional selection using
mapdirectives, or external solutions like Lua modules. The “Distributed Rate Limiting” section in the current article discusses these alternatives.Hello,
Code example for “Rate Limiting Based on Request Types” does not run on latest nginx plus version. receives below error,
nginx-1 | 2025/09/15 15:41:45 [emerg] 1#1: “limit_req” directive is not allowed here in /etc/nginx/nginx.conf:137
nginx-1 | nginx: [emerg] “limit_req” directive is not allowed here in /etc/nginx/nginx.conf:137
Thanks for reporting this, Sagar. You were absolutely right. The “Rate Limiting Based on Request Types” section that existed when you commented in September had a configuration that placed
limit_reqinside anifblock. Nginx does not allow thelimit_reqdirective in that context, which caused the error you saw.The article has since been completely rewritten. The current version uses
mapandgeodirectives to achieve request-type differentiation, which avoids theifblock limitation entirely. See the “Rate Limiting by User-Agent” and “Allowlisting Trusted IP Addresses” sections for working examples of conditional rate limiting.