How to Rate Limit in NGINX

Rate limiting controls how many requests a client can make to your server within a given time period. This protects your application from brute-force attacks, API abuse, and traffic spikes that could overwhelm backend services. Whether you need to throttle login attempts, limit API calls per user, or defend against DDoS-style floods, Nginx provides built-in rate limiting through the ngx_http_limit_req_module.

By the end of this guide, you will have working rate limit configurations that control request frequency, handle traffic bursts gracefully, return custom status codes, and allowlist trusted IP addresses. You will also know how to test your configuration and troubleshoot common issues.

Prerequisites

Before configuring rate limiting, ensure you have Nginx installed and running on your server. If you need to install Nginx first, see the appropriate guide for your distribution:

You should also have root or sudo access to edit Nginx configuration files, typically located in /etc/nginx/. After making configuration changes, always test the syntax before reloading:

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Understanding Rate Limit Directives

Nginx implements rate limiting using the โ€œleaky bucketโ€ algorithm. Requests fill a bucket at the allowed rate, and excess requests either queue (burst), get delayed, or receive an error response. Four core directives control this behavior, as documented in the official Nginx rate limiting module documentation.

Core Rate Limiting Directives

DirectiveContextPurpose
limit_req_zonehttpDefines a shared memory zone to store rate limiting state and sets the request rate.
limit_reqhttp, server, locationApplies the rate limit with optional burst and delay parameters.
limit_req_statushttp, server, locationSets the HTTP status code for rejected requests (default: 503).
limit_req_log_levelhttp, server, locationControls logging level for rate limit events (default: error).

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).
  • 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+): A hybrid approach where the first N burst requests are processed immediately, and remaining burst requests are delayed. For example, burst=20 delay=10 processes 10 requests instantly and delays the remaining 10.

Basic Rate Limiting Configuration

This section demonstrates a complete rate limiting setup that you can adapt to your needs. The configuration limits each client IP to 2 requests per second with a burst allowance of 5 requests.

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;

    # Your existing configuration...
}

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://backend;
    }
}

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 && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

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.

Rate Limiting Practical Examples

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://backend;
        }

        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        location / {
            limit_req zone=general burst=50 nodelay;
            proxy_pass http://backend;
        }
    }
}

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://backend;
        }
    }
}

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://backend;
        }
    }
}

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 Limiting by User-Agent

Some bots and crawlers can be rate limited more aggressively than regular traffic. Use the map directive to create a key that varies based on the User-Agent header:

http {
    map $http_user_agent $is_bot {
        default 0;
        ~*(Googlebot|Bingbot|Slurp|DuckDuckBot) 1;
        ~*(curl|wget|python|scrapy) 1;
    }

    map $is_bot $bot_limit_key {
        0 "";
        1 $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://backend;
        }
    }
}

This configuration applies two overlapping limits: a strict 1 request per second limit for known bots and crawlers, plus a more permissive 10 requests per second limit for all traffic. Regular users are effectively limited to 10 requests per second, while bots are limited to 1 request per second.

User-Agent strings are easily spoofed, so this approach is not a security control. It works well for reducing crawler load but should not be relied upon to block malicious actors who can change their User-Agent.

Configuring Logging for Rate Limits

Understanding when and why requests are being limited is essential for tuning your configuration. Nginx logs rate limit events to the error log at a configurable level.

Set the Log Level

The limit_req_log_level directive controls which events are logged. The default is error, which only logs rejected requests. Lower levels also log delayed requests:

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 only when requests are rejected
  • 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

Reading Rate Limit Log Entries

When rate limiting activates, Nginx writes entries to the error log. Check your error log to see rate limit events:

sudo tail -f /var/log/nginx/error.log | grep limiting

Rate limit log entries look like this:

2026/01/04 10:30:15 [error] 12345#0: *678 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.

Dry Run Mode for Testing

Nginx 1.17.1 introduced limit_req_dry_run, which tracks rate limit statistics without actually rejecting requests. This is useful for testing limits in production before enforcement:

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. Review the logs to verify your limits are appropriate, then disable dry run to enforce them.

Testing Your Rate Limit Configuration

After configuring rate limits, test them to verify they work as expected. This section covers two testing approaches: quick validation with curl and load testing with Apache Bench.

Quick Testing with curl

Use a shell loop to send rapid requests and observe rate limiting in action:

for i in {1..15}; do curl -s -o /dev/null -w "Request $i: %{http_code}\n" http://example.com/; 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 Testing with Apache Bench

For more comprehensive testing, use Apache Bench (ab) to simulate concurrent connections. Install it first if needed.

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:   2.534 seconds
Complete requests:      100
Failed requests:        85
   (Connect: 0, Receive: 0, Length: 85, Exceptions: 0)
Non-2xx responses:      85
Requests per second:    39.46 [#/sec] (mean)

In this example, 85 of 100 requests failed due to rate limiting (returned 429 or 503), confirming the limits are working. The โ€œLengthโ€ failures indicate responses with different body lengths (error pages versus normal content).

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

grep -r "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 a configuration error or silent failure.

Configuration not reloaded

After editing configuration, you must reload Nginx:

sudo nginx -t && 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. Common rate limiting errors include:

nginx: [emerg] unknown directive "limit_req_zone" in /etc/nginx/sites-enabled/example:5

This error means the directive is in the wrong context. limit_req_zone belongs in the http block.

nginx: [emerg] unknown "mylimit" burst zone in /etc/nginx/sites-enabled/example:15

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.

Advanced Considerations

Rate Limiting Behind a Reverse Proxy

When Nginx sits behind a load balancer or CDN, $binary_remote_addr shows the proxyโ€™s IP, not the clientโ€™s. You need to extract the real client IP from headers like X-Forwarded-For.

First, configure Nginx to trust the real IP header from your proxy:

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;

    # Rest of configuration...
}

With this configuration, $binary_remote_addr reflects the real client IP extracted from the X-Forwarded-For header. See the Nginx reverse proxy guide for more details on proxy configurations.

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 removes the least recently used entries to make room for new ones. This is normal behavior, but a zone that constantly evicts entries may not be accurately tracking all clients.

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. These require the OpenResty distribution or compiling Nginx with Lua support, which is beyond the scope of this guide.

Integration with Automated Blocking

Rate limiting provides protection during attacks, but for persistent abusers, you may want to block IPs entirely. Tools like fail2ban on Debian or fail2ban on Ubuntu can monitor Nginx logs for rate limit events and automatically block repeat offenders at the firewall level.

Fail2ban watches log files for patterns (like repeated 429 errors) and adds firewall rules to block the offending IP. This provides a layered defense: rate limiting slows down attackers, and fail2ban blocks those who persist.

Conclusion

You now have working rate limits in Nginx that control request frequency, handle bursts gracefully, and can differentiate between trusted and untrusted traffic. The key techniques covered include defining zones with appropriate rates, using burst and nodelay for better user experience, allowlisting internal networks, and monitoring with proper logging. Test your configuration thoroughly before deploying to production, and monitor your error logs to tune the limits for your traffic patterns.

For additional Nginx security hardening, consider setting up security headers and gzip compression to improve both protection and performance.

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

Leave a Comment

Let us know you are human: