Rate Limiting in Nginx: A limit_req Configuration Guide
Malicious users or automated scripts hammering a site will at best degrade the experience and at worst exhaust server resources. Nginx’s limit_req controls client request rates right at the proxy layer, stopping abusive traffic before it reaches the backend. This post runs three controlled experiments in an nginx:1.28 container to measure the behavior of the rate, burst, and nodelay parameters — how many requests each combination rejects and how much delay it adds. The data speaks for itself.
Use Cases
Three typical needs: mitigating DDoS — rate-limiting a single IP so attack traffic no longer punches straight through to the backend; capping API usage — controlling requests per second per client to protect the backend from abuse; protecting login and registration endpoints — suppressing brute force and bulk-registration scripts.
Configuration Example
1 | http { |
Two core directives: limit_req_zone defines the rule — $binary_remote_addr rate-limits by client IP (the binary form saves memory), zone=mylimit:10m sets aside 10MB of shared memory for rate-limiting state, and rate=1r/s sets the per-client rate; limit_req applies the rule to a location — burst=5 allows 5 burst requests to queue, and nodelay decides whether queued requests are handled immediately.
Test 1: burst + nodelay — Fast-Track the First N
Measured with rate=3r/s, burst=5 nodelay, firing 10 rapid requests at the same URL (test environment nginx/1.28.3, same below):

Result: 6 × 200 and 4 × 503. Request 1 consumes the bucket’s initial quota; requests 2–6 take the 5 burst slots and are passed through immediately (what nodelay means); from request 7 on, the quota is exhausted and they’re rejected. The nodelay mode suits endpoints that “allow short bursts but enforce a hard total cap” — the experience inside the burst window is unaffected, and anything beyond is rejected outright.
Test 2: No burst — Strict Rate Limiting
Same rate, burst removed (limit_req zone=z3;), another 10 rapid requests:

1 × 200 and 9 × 503 — with no burst buffer, a rate of 3r/s means any two requests less than 333ms apart are rejected. This shape only fits strongly protective scenarios (like a login endpoint defending against brute force); slapped onto a normal page, a user’s double-click will hit a 503.
Test 3: burst without nodelay — Delayed Queueing
burst=5 but no nodelay, another 10 rapid requests, this time recording each request’s duration:

All 10 returned 200 — but watch each request’s duration. Fired serially (measured): request 1 processed immediately at 0.0008 seconds, and each subsequent one steadily waited about 0.33 seconds longer — exactly the rate’s rhythm (1/3 second per request). Then let all 10 arrive simultaneously, and the behavior is even clearer: 1 passed immediately, and the 5 that entered the queue were released in sequence at 0.34 / 0.67 / 1.00 / 1.34 / 1.67 seconds — the queueing ceiling is burst × (1/rate) ≈ 1.7 seconds, and the remaining 4, beyond the burst, got straight 503s. That’s the semantics of the delay mode: over-rate burst requests aren’t rejected; they queue and are released at an even pace, and the cost is that queue depth times interval equals the maximum delay the client must endure. It fits “not in a hurry” traffic such as async task callbacks and crawler-friendly endpoints.
Keep the three modes side by side: strict rate limiting passes only requests within the rhythm and rejects the rest; nodelay passes the burst allowance immediately and rejects beyond it; delay mode queues the burst allowance and releases it evenly, rejecting beyond it.
Wrong-Way Comparison: The Rate-Limit Directive Gets “Jumped”
limit_req takes effect in the access phase. If the location uses return to produce content directly, the request ends in the earlier rewrite phase, and the rate limit is decorative — measured with limit_req + return 200, 10 rapid requests produced all 200s, zero rejections:

1 | # Wrong: return short-circuits in the rewrite phase; limit_req never runs |
Requests rejected by the rate limiter return 503 by default, but 429 (Too Many Requests) is semantically better for telling the client to retry: one line, limit_req_status 429;.
Notes and Cautions
- rate and burst must fit the business: too strict hurts real users, too loose loses the protection. Derive parameters from your real traffic distribution (P95 request interval), then canary-watch the 503/429 ratio.
- With multi-instance deployment, the limit multiplies by instance count: limit_req’s counters live in each instance’s memory — 4 machines means 4× the rate. Precise global rate limiting requires a centralized solution (e.g., Redis counters).
- Pick the right dimension for the rate-limit key:
$binary_remote_addris per-IP, but in scenarios where mobile egress IPs cluster (hundreds of users behind one corporate NAT), it hurts innocent users; rate-limit by$http_authorizationor a business token dimension instead. - How caching and rate limiting stack: if the same location also has proxy_cache, cache hits pass through limit_req’s counting first too — when load-testing cache-hit traffic, remember the rate limiter is consuming as well.
Summary
Back to the opening scenario: stopping abusive traffic before it reaches the backend — limit_req does it with two directives, but you must know the three modes’ behavioral differences cold. The tested conclusions in one line: strict rate limiting is a killer switch (over rate, rejected); burst+delay is a shaper (queue, release evenly); burst+nodelay is a buffer lane (within the allowance, pass; beyond, reject). Add limit_req_status 429 so rejection semantics are correct, and your rate limiting is genuinely in production.
This article was rebuilt from the author’s CSDN blog posts published between 2020 and 2024, originally published on CSDN.