Configure Nginx Rate Limiting with limit_req

Configure Nginx rate limiting with limit_req zones, bursts, header keys, custom 429 responses, RealIP, logs, and fixes.

Last updatedAuthorJoshua JamesRead time14 minGuide typeNginxDiscussion4 comments

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 variableContextDefault or version notePurpose
limit_req_zonehttpNo default; requires a key, zone, and rate.Defines a shared memory zone to store rate limiting state and sets the request rate.
limit_reqhttp, server, locationNo 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_statushttp, server, locationDefault 503; appeared in Nginx 1.3.15.Sets the HTTP status code for rejected requests.
limit_req_log_levelhttp, server, locationDefault error; appeared in Nginx 0.8.18.Sets the log level for rejected requests; delayed requests log one level lower.
limit_req_dry_runhttp, server, locationDefault off; appeared in Nginx 1.17.1.Counts excessive requests without delaying or rejecting them.
$limit_req_statusembedded variableAppeared 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_addr instead of $remote_addr saves 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/m for 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=10 lets 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:

  1. The geo block sets $limit to 0 for trusted networks and 1 for everyone else
  2. The map block converts this into a key: empty string for trusted IPs, client IP for others
  3. Requests with an empty key are not tracked by the rate limiter, effectively bypassing it

Add your actual internal network ranges to the geo block. 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 error and delayed requests at warn
  • 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 as 429 or 503.
  • 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-traffic can 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.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show more of our fresh Linux tutorials in Top Stories and From your sources when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee

4 thoughts on “Configure Nginx Rate Limiting with limit_req”

  1. 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;
    }
    }
    }

    Reply
    • 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_req inside an if block. Nginx does not allow the limit_req directive in that context, which caused the error you saw.

      The article has since been completely rewritten. The current version uses map and geo directives to achieve conditional rate limiting, which avoids the if block limitation entirely. See the “Rate Limit POST and Write Requests”, “Rate Limit Noisy User Agents”, and “Allowlisting Trusted IP Addresses” sections for working examples.

      Reply
  2. 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

    Reply
    • Thanks for the follow-up, Sagar. You were correct again. The rate parameter in limit_req_zone requires a constant value. Nginx does not support variables there, so the rate=$dynamic_rate example 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 map or geo, 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.

      Reply
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<a href="https://example.com">link</a> link
<blockquote>quote</blockquote> quote block

Got a Question or Feedback?

We read and reply to every comment - let us know how we can help or improve this guide.

Verify before posting: