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. From that baseline, you 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.
Return a Custom 429 Rate Limit Response
API clients usually handle a clear 429 response better than a generic error page. Pair limit_req_status 429 with an internal named location when you want a compact JSON response for rejected requests. This example assumes the mylimit zone from the server-wide setup already exists in the http block:
server {
listen 80;
server_name example.com;
error_page 429 = @rate_limited;
location @rate_limited {
default_type application/json;
return 429 '{"error":"rate_limited","message":"Too many requests"}';
}
location /api/ {
limit_req zone=mylimit burst=5 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
The named location keeps the response internal, so direct browser requests to /@rate_limited are not part of the public URL space. Keep the message short and avoid exposing backend details such as queue depth, account state, or upstream error text.
Practical Nginx Rate Limiting Examples
After adapting an example, 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;
limit_req_status 429;
location /login {
limit_req zone=login burst=3;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api burst=20 nodelay;
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.
Rate Limit POST and Write Requests
Write methods often deserve a stricter limit than ordinary page views because they can trigger login checks, database writes, uploads, or expensive API work. Do not place limit_req inside an if block; create a conditional key with map, then leave non-matching methods untracked with an empty key.
http {
map $request_method $write_limit_key {
default "";
~^(POST|PUT|PATCH|DELETE)$ $binary_remote_addr;
}
limit_req_zone $write_limit_key zone=write_methods:10m rate=2r/s;
server {
listen 80;
server_name example.com;
location /api/ {
limit_req zone=write_methods burst=5 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
}
Only POST, PUT, PATCH, and DELETE requests receive a rate-limit key in this example. GET and HEAD requests produce an empty key, and Nginx does not count requests with an empty key in the rate-limit zone. For broader conditional logic, compare this pattern with Nginx if and else directive alternatives.
Rate Limit API Keys or Trusted Headers
Authenticated APIs may need fairness by account, tenant, or API key instead of only by IP address. Use a header key only when the value is short, normalized, and trusted, or when a companion per-IP limit still protects the public edge. A client-supplied header by itself is easy to rotate.
http {
map $http_x_account_id $account_limit_key {
default $http_x_account_id;
"" $binary_remote_addr;
}
limit_req_zone $account_limit_key zone=account_api:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=api_ip:10m rate=100r/s;
server {
listen 80;
server_name example.com;
location /api/ {
limit_req zone=account_api burst=40 nodelay;
limit_req zone=api_ip burst=100 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
}
}
Requests with an X-Account-Id header share the account-level limit, while requests without that header fall back to the client IP. The separate api_ip zone acts as a higher abuse ceiling for clients that rotate or forge header values. Avoid using long bearer tokens as rate-limit keys; prefer a stable account identifier injected by a trusted gateway before traffic reaches this Nginx layer.
Log Nginx Rate Limiting Decisions
Logging shows whether Nginx is rejecting traffic, delaying bursts, or only 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 grep in Linux with tail in Linux to inspect recent matching lines while testing. For broader log inspection, use the Nginx access and error logs guide.
sudo grep "limiting requests" /var/log/nginx/error.log | tail -20
Relevant log lines include the rate-limit phrase, excess value, zone, client, request, and host:
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;
}
}
}
Define log_format in the http context. If a server or location block already sets its own access_log, update that specific log directive to use the rate_limit format; an inherited http-level access log will not replace a more specific one.
After sending a request that triggers the limit, check the access log for the structured result:
sudo grep "limit_req=REJECTED" /var/log/nginx/access.log | tail -5
Relevant output contains the configured status and 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 "%{http_code}\n" "$TARGET"; done | sort | uniq -c
With a rate limit of 2 requests per second and burst of 5, example output can look like this:
5 200
10 429
The first several requests succeed because the burst allowance lets a short spike through before limiting kicks in. Later requests receive 429 responses. If you configured limit_req_status 503 or left the default unchanged, the rejected count would appear under 503 instead. The exact split depends on request timing because 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 and RHEL Family Systems
sudo dnf install httpd-tools
Other distributions may use a different package name for Apache Bench. If neither package exists, search your distribution’s package index for the package that provides the ab command.
Then run a test with 100 requests at concurrency 10:
ab -n 100 -c 10 http://example.com/
Review these fields in the Apache Bench output:
Complete requests: Confirms how many requests finished.Non-2xx responses: Counts rate-limit responses such as429or503.Failed requests: Can include response-length differences, so read it with the status-code count.Requests per second: Shows the generated test rate, not a recommended production limit.
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.
For Cloudflare, load balancers, and multi-proxy chains, use the more specific restore real client IPs in Nginx workflow before rate limiting by IP. A broad set_real_ip_from 0.0.0.0/0 setting lets clients spoof the address used as the rate-limit key.
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
limit_req_zone Directive Is Not Allowed Here
The limit_req_zone directive belongs in the http context. If it is placed inside a server or location block, nginx -t fails before the configuration can be reloaded.
sudo nginx -t
Relevant output includes:
nginx: [emerg] "limit_req_zone" directive is not allowed here nginx: configuration file /etc/nginx/nginx.conf test failed
Find every limit_req_zone entry with line numbers:
sudo grep -R -n "limit_req_zone" /etc/nginx/
Move the zone definition to the main http block, then leave limit_req in the server or location block where the limit should apply:
http {
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
server {
location /api/ {
limit_req zone=mylimit burst=10 nodelay;
limit_req_status 429;
}
}
}
Retest before reloading:
sudo nginx -t
Zero Size Shared Memory Zone Error
The zero size shared memory zone error means a limit_req directive references a zone name that Nginx did not create. This usually comes from a spelling mismatch, a missing limit_req_zone, or a variable used where Nginx requires a literal zone or rate value.
sudo nginx -t
Relevant output includes:
nginx: [emerg] zero size shared memory zone "general" nginx: configuration file /etc/nginx/nginx.conf test failed
Compare zone definitions and zone references across the active config tree:
sudo grep -R -nE "limit_req(_zone)?|zone=" /etc/nginx/
Either create the missing zone in the http block or change limit_req to reference an existing zone name:
http {
limit_req_zone $binary_remote_addr zone=general:10m rate=5r/s;
server {
location / {
limit_req zone=general burst=10 nodelay;
limit_req_status 429;
}
}
}
The rate value must also be literal, such as 5r/s or 30r/m. A dynamic value such as rate=$dynamic_rate fails with an invalid rate error during the configuration test. Retest after fixing the zone name or rate value:
sudo nginx -t
Rate Limits Do Not Trigger After Reload
If nginx -t succeeds but requests are never delayed or rejected, confirm that Nginx loaded the file you edited. The active config dump should show both the owning file marker and the relevant rate-limit directives:
sudo nginx -T 2>/dev/null | grep -n -E '# configuration file|limit_req'
If your edited file does not appear, place it under an included directory such as /etc/nginx/conf.d/, or enable the site according to your distribution’s Nginx layout. Retest and reload only after the active dump contains the expected directives:
sudo nginx -t
sudo systemctl reload nginx
Legitimate Users Receive 429 Responses
A normal page load can request HTML, CSS, JavaScript, images, fonts, and API calls in a short burst. Corporate NATs, VPNs, and mobile carriers can also place many users behind one source address. If real users receive 429 responses, inspect the busiest client addresses before changing the limit:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
Then check recent rate-limit log entries for the affected path and zone:
sudo grep "limiting requests" /var/log/nginx/error.log | tail -20
Raise the burst for normal traffic spikes, narrow the limit to expensive endpoints, or use separate zones for login, API, and general traffic. For example, a higher burst on an API route lets short client bursts pass while preserving the base rate:
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:3000;
}
Retest with a short request loop and keep watching logs before tightening the rate again.
All Clients Share One Rate Limit Behind a Proxy
When every request appears to come from a CDN, load balancer, or reverse proxy, the rate limiter protects the proxy address instead of the real client address. Confirm the top logged source before changing rate values:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
If the top address is your proxy or CDN edge, configure RealIP for trusted proxy ranges first, then keep rate-limit zones keyed on the restored client address. Do not trust all source ranges; only controlled proxy addresses should be allowed to set the client IP header.
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 related Nginx work, 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 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 conditional rate limiting, which avoids theifblock limitation entirely. See the “Rate Limit POST and Write Requests”, “Rate Limit Noisy User Agents”, and “Allowlisting Trusted IP Addresses” sections for working examples.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 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 invalid example is no longer present. For dynamic rate limiting in Nginx, use multiple literal-rate zones with conditional keys from
maporgeo, or use a separate architecture such as Lua/OpenResty when you need external state. The “Rate Limit POST and Write Requests” and “Distributed Rate Limiting” sections in the current article discuss those alternatives.