Guides

Rate limiting on NGINX: configuring limit_req without blocking real users

How to configure limit_req_zone and limit_req on NGINX, test with nginx -t and verify in logs without blocking legitimate traffic.

UptimeMag editorial team · 24 October 2026 · 4 min read

Rate limiting su NGINX: configurare limit_req senza bloccare gli utenti veri

A login endpoint under brute-force attack, a scraper hammering the same page, a bot ignoring robots.txt: the symptom is always the same—CPU maxed out and real users waiting. NGINX rate limiting solves the problem with two directives, but requires tuning: a threshold set too low will block legitimate users as well. This guide configures limit_req_zone and limit_req, tests them with nginx -t and verifies the results in logs, on Ubuntu 24.04.

Declared version

The tests were performed with NGINX installed via apt on Ubuntu 24.04. The official Ubuntu repository for this release packages nginx version: nginx/1.24.0 (Ubuntu), confirmed also by Vultr documentation with the same output from nginx -version. Those wanting a more recent version should switch to the official nginx.org repository (stable or mainline): it's a different package with a different update path, and should be chosen deliberately.

The two directives: limit_req_zone and limit_req

The configuration fits in two lines. The first, limit_req_zone, goes in the http block and defines the shared memory zone, the counting key (usually the client's IP) and the threshold:

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

The syntax is limit_req_zone key zone=name:size rate=rate [sync], and sets the parameters for a shared memory zone that maintains state across various keys, specifically the current number of exceeding requests; the key can contain text, variables or their combination; requests with an empty key value are not counted. This means you can build an allowlist by zeroing out the key for internal IPs, instead of excluding them with an if.

The second directive, limit_req, applies the zone where needed—http, server or location:

server {
 location /login/ {
 limit_req zone=mylimit;
 proxy_pass http://backend;
 }
}

On its own, without additional parameters, it rejects every request exceeding the average threshold: with rate=10r/s the tenth request in the same second from the same IP receives 503.

Choosing the threshold without blocking legitimate traffic

Real traffic never arrives at regular intervals: a browser loads a page and instantly downloads CSS, JS, images, all from the same IP. This is where burst comes in:

limit_req zone=mylimit burst=20 nodelay;

With rate=1r/s and burst=5 you allow no more than 1 request per second on average, with peaks not exceeding 5 requests. Without nodelay requests exceeding the average rate are queued and processed with delay; if you don't want to delay excess requests use the nodelay parameter, while the delay parameter (from version 1.15.7) sets the limit above which excess requests start to be delayed, and the default value is zero, meaning all excess requests are delayed. A middle ground, used for endpoints receiving wave-like traffic such as a search page, is explicit delay: limit_req zone=ip burst=12 delay=8 lets the first 8 excess requests through immediately and queues only those beyond.

For different endpoints it's better to separate the zones rather than use a single one that's either too broad or too tight:

Endpoint Zone rate burst Behaviour
Login / form login 1r/s 5 nodelay, rejects immediately above threshold
Generic API api 10r/s 20 short queue, absorbs browser peaks
Static site general 50r/s 50 limit only against aggressive scraping

Always start with loose thresholds and tighten them by watching logs, not the other way round: a threshold guessed at the drawing board risks blocking real users behind corporate NAT or proxy, where many people share the same IP.

Testing the configuration before reload

limit_req_zone is valid only in the http context; if it accidentally ends up inside a server or location block, nginx -t fails before the configuration can be reloaded, with a message like "limit_req_zone" directive is not allowed here. For this reason the command must always be run before reload:

sudo nginx -t
sudo systemctl reload nginx

If the test passes, reload won't interrupt already-open connections.

Verifying the limit is working

Before putting an aggressive threshold into production, it's worth observing it without actually rejecting anything. NGINX offers limit_req_dry_run: with this directive active, every request that would exceed the limit is logged with the "dry run" flag, without actually blocking traffic. It's the way to understand how tight a threshold is before activating it for real.

Once blocking is enabled, verification is in the response codes: with burst and nodelay, requests exceeding the burst are rejected with error 503, so a quick check is:

grep ' 503 ' /var/log/nginx/access.log | tail -50

If code 503 appears also for IPs that aren't bots but real browsers, the threshold is too low and should be raised. To distinguish rate limiting from other 503s (backend down, for example) you can change the returned code with limit_req_status, setting it to 429 for instance, the HTTP code specifically designed for "too many requests", and then filter logs on that value.

This guide does not concern Prime Software products.

Written with the help of artificial intelligence and checked by the editors (EU AI Act, art. 50).