Safeguard
Security Guides•

nginx limit_req: what rate, burst and nodelay really do

How nginx limit_req works: the zone, the rate, burst and nodelay, why rejected requests return 503 by default, and how to test a limit with dry run.

Safeguard Security Team
4 min read

nginx limit_req uses a shared memory zone that tracks requests per key, usually the client address. The rate sets the steady speed, burst adds a queue of extra requests, and nodelay serves that queue at once instead of spacing it out. Anything past the burst is rejected, with status 503 unless you change it.

The two directives you always need

Rate limiting takes two lines. The first defines a zone in the http block, and the second applies it to a server or location.

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;

location /login {
    limit_req zone=login burst=10 nodelay;
    limit_req_status 429;
}

The key is the part people skip past. The docs say requests with an empty key value are not accounted, so a key built from a variable that is sometimes empty silently exempts those requests. The docs also recommend $binary_remote_addr over $remote_addr because its size is always 4 bytes for IPv4 and 16 bytes for IPv6.

How rate and burst interact

The rate is written in requests per second (r/s). For less than one per second, use requests per minute, so half a request per second is 30r/m.

Burst defaults to zero. With no burst, a request that arrives faster than the rate is rejected. With burst=5, the docs describe a limit of not more than one request per second on average, with bursts not exceeding 5 requests. Excess requests inside the burst are delayed until their number passes the maximum, then the request is terminated with an error.

That delay is a feature for polite clients and a cost for busy ones. A browser loading a page fires many requests in a moment, so a strict rate with no burst will break normal pages.

What nodelay changes

By default, requests inside the burst wait so the overall rate stays smooth. With nodelay, they are answered immediately, and the burst slots refill at the configured rate. The result is a limit that feels invisible to normal users but still cuts off sustained floods.

There is also a delay=number form (added in 1.15.7). It sets the point at which excess requests start being delayed, so the first few are fast and the rest are spaced out.

Choose the status code on purpose

The default for limit_req_status is 503. A 503 looks like a server fault to monitoring and to clients, which may retry harder. Setting 429 tells clients and your own dashboards that the limit fired. Our post on telling a 429 apart covers how clients should react.

Test before you enforce

limit_req_dry_run is off by default. When on, requests are not limited, but the excess is still counted in the zone, and the $limit_req_status variable reports values such as PASSED, REJECTED_DRY_RUN and DELAYED_DRY_RUN. Add that variable to your log format, run a day of real traffic, and see who would have been blocked.

Watch memory too. The docs say one megabyte holds about 8 thousand 128-byte states on 64-bit platforms, and if the zone is exhausted the least recently used state is removed. If a new state still cannot be created, the request is terminated with an error. A tiny zone behind a busy site can start failing requests for the wrong reason.

One more trap: several limit_req directives are inherited from the previous level only if the current level defines none. Add one in a location and the server level limits stop applying there.

Where this sits with other defenses

Per address limits do not stop an attacker with many addresses, and they punish users behind shared NAT. Treat nginx limits as one layer, next to account level controls. The API resource limits checklist lists the others.

Safeguard does not tune your proxy. It covers what sits behind it: SBOM and dependency scanning, reachability analysis and policy gates for the code you deploy. The free scan at https://gold.safeguard.sh/scan is a quick way to see your dependency exposure.

Quick check

What status does nginx return for a rejected limit_req request by default?
  1. Set limit_req_status to 429 if you want clients and dashboards to see a rate limit signal.
Does nodelay raise the allowed rate?

No. It only stops nginx from spacing out the burst. The long run rate is still the zone rate.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.