Rate-limit NGINX requests with limit_req, burst, and stub_status monitoring
How to configure NGINX limit_req with a shared-memory zone, burst queue, and real-time monitoring via stub_status to tame traffic spikes without silently dropping requests.
24 Feb 2026, 02:59 UTC

The throttling puzzle
You're seeing 503 responses on your NGINX proxy during traffic that looks normal in your application logs. Requests that should succeed are being rejected, and the access log only shows the status code, not whether the decision came from your rate-limiting configuration or upstream. This pattern is common when limit_req is active but the shared-memory zone is too small, or when the burst setting isn't aligned with the actual traffic surge you're trying to absorb.
1. defining the rate-limiting zone
The foundation of any NGINX rate limit is the limit_req_zone directive. It creates a shared-memory region that NGINX uses to track request rates per client key. The directive's simplest form looks like this:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
Here, $binary_remote_addr is the key NGINX uses to identify each client. The zone=api_limit:10m part allocates 10 megabytes of shared memory, which can hold roughly 160000 client identifiers (roughly 64 bytes per entry). The rate=10r/s sets the maximum rate to 10 requests per second per key. If the zone fills because too many distinct clients are making requests, NGINX will refuse new configurations with an error, so sizing the zone for your expected concurrency is the first practical step.
2. enforcing the rate with burst control
Once the zone exists, the limit_req directive applies it to a location block:
location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://backend; }
The burst=20 parameter tells NGINX to allow up to 20 requests to accumulate in a queue when the rate limit is exceeded. If the queue fills, further requests get a 503 response. The nodelay flag means excess requests are processed immediately up to the burst capacity, without waiting. Without nodelay, NGINX will hold requests in the queue, adding latency. With it, requests that fit within the burst are served right away, which is useful for short-lived spikes such as a burst of API calls from a mobile app.
If you omit burst entirely, NGINX will immediately return 503 for any request beyond the configured rate. Choosing a burst size requires thinking about your typical spike pattern: too small and legitimate bursts are dropped; too large and you've effectively disabled the limit during spikes.
3. watching acceptance counts in real time
NGINX can expose rate-limit metrics through the ngx_http_stub_status_module. After enabling the module and adding a status location, the status page shows accepted and rejected request counts for each limit_req_zone. To check if it's compiled in, run nginx -V and verify that ngx_http_stub_status_module appears in the output. Then add a location block such as:
location = /nginx_status { stub_status; access_log off; allow 127.0.0.1; deny all; }
Navigate to http://your-host/nginx_status in a browser or with curl to see the status page counters, which list total requests, handled, reading, writing, waiting, and crucially for rate limiting the limit_req accepted and rejected counters per zone. This lets you confirm whether your burst setting is absorbing traffic or if the limit is consistently triggering rejections.
4. trade-offs and practical tuning
- Zone size: a 10m zone tracks about 160000 unique client keys. If your traffic pattern involves more distinct IP addresses, increase the zone (e.g., 20m) before you hit the zone overflow error.
- Aggressive rates: setting a rate that's too low for your legitimate traffic will cause constant 503s, especially during morning ramp-up or promotional events. Start with a rate that covers your baseline, then add burst to handle spikes.
- Burst overflow: if burst fills and nodelay is off, requests queue and increase latency. If nodelay is on, they're served immediately up to the burst cap, which may be desirable for latency-sensitive APIs but can reduce the effectiveness of the limit.
A practical verification loop: after applying a config, run nginx -T to confirm the directives are parsed without error. Then use curl to send requests faster than the defined rate, e.g., for i in {1..15}; do curl -s -o /dev/null -w '%{http_code} '; done against the rate-limited location. You should see a mix of 200 and 503 codes. Finally, open the stub_status page and check that the limit_req rejected counter increments as expected.
If the rejected count stays at zero while you're sending traffic above the rate, the zone may be too large for the concurrent client count, or the rate setting isn't being enforced as intended. If every request returns 503, reduce the rate or increase the burst.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.