How to Rate Limit in NGINX

Last updated Monday, April 27, 2026 4:16 pm Joshua James 11 min read 4 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. 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 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.

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:

  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.

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 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 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-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

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.

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 “How to Rate Limit in NGINX”

  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 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 section removed entirely. For dynamic rate limiting in Nginx, you need either multiple zones with conditional selection using map directives, or external solutions like Lua modules. The “Distributed Rate Limiting” section in the current article discusses these alternatives.

      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 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 request-type differentiation, which avoids the if block limitation entirely. See the “Rate Limiting by User-Agent” and “Allowlisting Trusted IP Addresses” sections for working examples of conditional rate limiting.

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

Let us know you are human: