Operations 21 min read

Production-Ready Nginx Rate Limiting: Stop Crawlers, Block Malicious Requests, Protect APIs

This guide explains how to use Nginx's built‑in limit_req and limit_conn modules to implement production‑grade rate limiting, covering leaky‑bucket theory, IP and API‑key based limits, burst and nodelay handling, whitelist bypass, custom error responses, dry‑run testing, memory sizing, and the inherent limitations that may require Redis‑Lua or OpenResty extensions.

Ops Community
Ops Community
Ops Community
Production-Ready Nginx Rate Limiting: Stop Crawlers, Block Malicious Requests, Protect APIs

Problem Background

Typical production scenarios that require Nginx rate limiting include:

Malicious script attacks on login, registration or order endpoints.

Excessive crawling by search engine bots or custom scrapers that exhaust backend resources.

Traffic spikes caused by promotions or hot events that exceed backend capacity.

Single‑user abuse where an API consumer exceeds a reasonable request frequency.

Core Principle: Leaky‑Bucket Algorithm

The limit_req module implements a leaky‑bucket algorithm. The three parameters are:

rate – the constant outflow, i.e., allowed requests per second (or per minute).

burst – the bucket capacity that buffers short‑term spikes.

nodelay / delay – whether excess requests are queued (delayed) or rejected immediately.

Compared with a token‑bucket, the leaky‑bucket enforces a strict throttling rate, while the token‑bucket permits bursts. By using burst together with nodelay, Nginx can emulate token‑bucket behaviour.

Configuration Directives

limit_req_zone

(http scope) – defines a shared memory zone and the request rate.

http {
    limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s;
}
$binary_remote_addr

stores the client IP in binary form (4 bytes for IPv4, 16 bytes for IPv6) and saves memory compared with $remote_addr. zone=name:size allocates shared memory; on a 64‑bit system each state consumes ~128 bytes (≈80 k clients per 10 MB). rate=10r/s sets the allowed request rate; units r/s or r/m are supported. limit_req (http/server/location) – enables rate limiting in the specified context.

location /api/ {
    limit_req zone=per_ip burst=20 nodelay;
}
zone

selects the previously defined zone. burst defines how many requests can exceed rate and be processed immediately. nodelay lets burst requests pass without queuing; without it, excess requests are delayed, increasing response time. delay=N (available since 1.15.7) adds fine‑grained control: the first N excess requests are not delayed, subsequent ones are. limit_req_status – custom response code for rejected requests (default 503, commonly set to 429). limit_req_log_level – log level for rejected or delayed requests. limit_req_dry_run (1.17.1+) – dry‑run mode that only records statistics without enforcing limits.

Basic Configuration Examples

IP‑Based Limiting (most common)

http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    server {
        listen 80;
        server_name api.example.com;
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

Effect:

Each IP can make up to 10 requests/s.

Up to 20 burst requests are processed instantly.

Requests beyond 10 r/s + 20 burst receive 429.

Without nodelay, excess requests are queued, causing increasing latency.

API‑Key Based Limiting (multi‑tenant)

http {
    limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/m;
    server {
        location /v1/ {
            if ($http_x_api_key = "") { return 401; }
            limit_req zone=per_key burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

The variable $http_x_api_key maps the X-API-Key request header to a binary key.

Combined IP + Key Limiting

http {
    limit_req_zone $binary_remote_addr zone=per_ip:10m rate=5r/s;
    limit_req_zone $http_x_api_key zone=per_key:10m rate=100r/m;
    server {
        location /v1/ {
            limit_req zone=per_ip burst=10 nodelay;
            limit_req zone=per_key burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

Both limit_req directives are AND‑ed; a request must satisfy both limits.

Advanced Scenarios

Fine‑Grained delay Control

location /api/ {
    limit_req zone=per_ip burst=12 delay=8;
    # rate=10r/s, burst=12, delay=8: first 8 excess requests pass, later ones are delayed
}

Behavior:

≤ 10 requests – all processed normally.

11‑18 requests – 10 normal, next ≤ 8 pass immediately.

19‑22 requests – 10 normal, next ≤ 8 pass, remaining delayed.

> 22 requests – 10 normal, ≤ 8 pass, excess delayed or rejected (429).

Whitelist Bypass (geo + map)

http {
    geo $limit {
        default 1;
        127.0.0.1 0;
        10.0.0.0/8 0;
        172.16.0.0/12 0;
        192.168.0.0/16 0;
    }
    map $limit $limit_key {
        0 "";          # empty key → no limiting
        1 $binary_remote_addr;
    }
    limit_req_zone $limit_key zone=whitelist:10m rate=10r/s;
    server {
        location /api/ {
            limit_req zone=whitelist burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

When $limit_key is empty, limit_req_zone does not create a state, so the request bypasses rate limiting.

Custom Error Responses

http {
    limit_req_status 429;
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    server {
        error_page 429 = @rate_limit;
        location @rate_limit {
            internal;
            default_type application/json;
            add_header Retry-After 5;
            return 429 '{"code":429,"message":"Too many requests, please retry later","retry_after":5}';
        }
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

Method 2 uses a static JSON file via error_page 429 /429.json for lower overhead.

URI‑Based Differential Limiting

http {
    limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;
    limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
    limit_req_zone $binary_remote_addr zone=static:10m rate=100r/s;
    server {
        location /api/login/ { limit_req zone=login burst=5 nodelay; limit_req_status 429; proxy_pass http://backend; }
        location /api/ { limit_req zone=api burst=30 nodelay; limit_req_status 429; proxy_pass http://backend; }
        location /static/ { limit_req zone=static burst=200 nodelay; limit_req_status 429; root /var/www/static; }
    }
}

Dry‑Run Mode (1.17.1+)

http {
    limit_req_zone $binary_remote_addr zone=dry:10m rate=10r/s;
    server {
        location /api/ {
            limit_req_dry_run on;
            limit_req zone=dry burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

In dry‑run, Nginx logs which requests would have been limited without rejecting them. The variable $limit_req_status (1.17.6+) reports statuses such as PASSED, DELAYED, REJECTED, DELAYED_DRY_RUN, REJECTED_DRY_RUN.

Concurrent Connection Limiting (limit_conn)

http {
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
    server {
        location /api/ {
            limit_conn conn_per_ip 10;
            limit_conn_status 429;
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}
limit_conn

caps simultaneous connections per IP, useful for download or WebSocket limits.

Configuration Validation & Testing

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

Load testing with ab or wrk shows how many requests are rejected (429) versus passed.

Memory Estimation

Each client state occupies ~128 bytes. Approximate capacity:

1 MB ≈ 8 000 clients
10 MB ≈ 80 000 clients
100 MB ≈ 800 000 clients

Insufficient zone size leads to state eviction and inaccurate limiting.

Limitations & Alternative Solutions

State not shared across nodes – each Nginx instance tracks its own counters, causing inaccurate cluster‑wide limits. Alternative: Redis + Lua or a centralized rate‑limit gateway.

Counters reset on restart – shared‑memory zones are cleared when Nginx restarts. Alternative: external storage (e.g., Redis) for persistence.

No sliding‑window time‑range – cannot express "N requests per hour". Alternative: custom Lua scripts.

Limited key granularity – only IP, headers, URI, etc., are supported. Alternative: OpenResty + Lua for richer keys.

Redis + Lua Distributed Limiting

-- Simple sliding‑window limit
local key = "rate_limit:" .. ngx.var.binary_remote_addr
local limit = 10   -- requests per second
local window = 1   -- seconds
local red = redis:new()
red:set_timeout(100)
local ok, err = red:connect("127.0.0.1", 6379)
local current = red:incr(key)
if current == 1 then red:expire(key, window) end
if current > limit then ngx.exit(429) end

Production Deployment Tips

Enable monitoring for 429 status before rolling out limits.

Start with limit_req_dry_run for at least one business cycle.

Set thresholds with headroom (e.g., peak QPS = 1000 → limit = 1200 with burst).

Return friendly JSON messages for critical APIs instead of raw 429 pages.

Combine rate limiting with WAF, CDN, or DDoS protection for attacks exceeding Nginx capacity.

Conclusion

The three essential elements of Nginx rate limiting are:

1. limit_req_zone (define rule)
2. limit_req (apply rule)
3. limit_req_status (custom reject code)

Recommended configurations per scenario:

Login endpoint – rate=2r/s burst=5 nodelay General API – rate=20r/s burst=30 nodelay Multi‑tenant API – key‑based zone with per‑key rate.

Internal system – IP whitelist to skip limiting.

Big promotion – temporarily lower rate and use error_page for a queue page.

Best practices derived from the analysis:

Use 429 for limit responses; it is easy to monitor.

Prefer delay=N for graceful throttling of bursts.

Store keys as $binary_remote_addr to save memory.

Employ geo + map for whitelist bypass.

Validate new policies in dry‑run mode before production.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Nginxrate limitingleaky bucketanti‑crawlerlimit_connlimit_reqAPI protection
Ops Community
Written by

Ops Community

A leading IT operations community where professionals share and grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.