Skip to content

burst without nodelay does not delay requests — delay mechanism is non-functional #22

@dvershinin

Description

@dvershinin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions