When using burst=N without nodelay, excess requests within the burst allowance should be delayed (throttled) to match the configured rate, identical to native limit_req behavior. Instead, all burst requests pass through immediately with no delay — the behavior is always equivalent to nodelay regardless of whether it is specified.
The burst parameter itself works correctly (allows N excess requests before blocking the IP in Redis). Only the delay/throttle behavior is affected.
Steps to Reproduce
http {
dynamic_limit_req_zone $binary_remote_addr zone=test:10m rate=1r/s redis=127.0.0.1 block_second=300;
server {
listen 80;
root /var/www/html;
location / {
dynamic_limit_req zone=test burst=5; # no nodelay
dynamic_limit_req_status 429;
}
}
}
# Send 3 rapid requests
for i in 1 2 3; do
time curl -s -o /dev/null -w "%{http_code}" http://server/
done
Expected behavior (matching native limit_req)
Request 1 completes instantly (~0ms). Requests 2 and 3 are delayed ~1 second each to match rate=1r/s.
Actual behavior
All 3 requests complete instantly (<1ms). No throttling occurs.
Root Cause
In the request handler, when ngx_http_limit_req_lookup returns NGX_OK (request within burst, last zone with account=true), the Redis GET check at line 276–279 short-circuits the handler before the delay logic at line 317–342 is reached:
// Line 275-281: This returns NGX_DECLINED (allow) before delay code runs
if (!c->err) {
reply = redisCommand(c, "GET %s", Host);
if (reply->str == NULL) { // IP not in Redis (no SETEX was done for NGX_OK)
freeReplyObject(reply);
return NGX_DECLINED; // <-- exits here, delay logic never reached
}
}
// Line 317-342: delay logic — dead code, never reached for NGX_OK
if (rc == NGX_AGAIN) {
excess = 0;
}
delay = ngx_http_limit_req_account(limits, n, &excess, &limit);
if (!delay) {
return NGX_DECLINED;
}
// ... timer setup for delay ...
When rc == NGX_OK (within burst), line 246 if (rc && ...) is false (NGX_OK is 0), so no SETEX is performed. The subsequent GET finds nothing and returns NGX_DECLINED immediately.
Suggested Fix
Move the delay logic before the Redis GET check, or restructure the handler so that the NGX_OK path computes the delay before consulting Redis. The ngx_http_limit_req_account function and delay timer setup (lines 317–342) are already correct — they just need to be reachable.
Environment
- Module version: 1.9.3
- NGINX version: 1.28.0 (tested on Rocky Linux 10, aarch64)
- Redis: Valkey 8.x
When using
burst=Nwithoutnodelay, excess requests within the burst allowance should be delayed (throttled) to match the configured rate, identical to nativelimit_reqbehavior. Instead, all burst requests pass through immediately with no delay — the behavior is always equivalent tonodelayregardless of whether it is specified.The
burstparameter itself works correctly (allows N excess requests before blocking the IP in Redis). Only the delay/throttle behavior is affected.Steps to Reproduce
Expected behavior (matching native
limit_req)Request 1 completes instantly (~0ms). Requests 2 and 3 are delayed ~1 second each to match
rate=1r/s.Actual behavior
All 3 requests complete instantly (<1ms). No throttling occurs.
Root Cause
In the request handler, when
ngx_http_limit_req_lookupreturnsNGX_OK(request within burst, last zone withaccount=true), the RedisGETcheck at line 276–279 short-circuits the handler before the delay logic at line 317–342 is reached:When
rc == NGX_OK(within burst), line 246if (rc && ...)is false (NGX_OKis 0), so noSETEXis performed. The subsequentGETfinds nothing and returnsNGX_DECLINEDimmediately.Suggested Fix
Move the delay logic before the Redis
GETcheck, or restructure the handler so that theNGX_OKpath computes the delay before consulting Redis. Thengx_http_limit_req_accountfunction and delay timer setup (lines 317–342) are already correct — they just need to be reachable.Environment