Skip to content

Session queue

When all -limit slots are busy, a new request queues. How long it may wait before giving up with 429 is decided by a single number: how long since a request was admitted quickly. Call it idle.

  • A fast admission resets idle to zero.
  • Nothing else resets it: not a slow admission, not a refusal, not the queue emptying.
idleStateBudget given to a newly queued request
≤ -queue-timeoutKeeping up-queue-timeout
> -queue-timeoutSaturated-queue-congestion-timeout

That is the entire rule. You normally set one flag:

Terminal window
websummoner -limit 2 -queue-timeout 5m

and the three numbers it works with are derived from it:

NumberDerived asWith -queue-timeout 5m
How fast an admission must be to count-queue-timeout / 20, minimum 1s15s
Budget while keeping up-queue-timeout itself5m
Budget while saturated-queue-congestion-timeout, which defaults to -queue-timeout / 10, minimum 1s30s

Only the last one has its own flag; set -queue-congestion-timeout to override it, and an explicit value is used as given rather than floored. The other two always follow -queue-timeout, so there is one knob to tune in practice.

Leaving -queue-timeout unset disables all of this: requests wait indefinitely until the client gives up, as Selenoid did.

Grid keeping up. Sessions finish, so admissions stay fast and keep resetting idle:

TimeEventidle afterBudget given
10:00:00A and B admitted, no wait0—
10:00:04C queues4s5m
10:00:09A ends, C admitted after 5s — fast0—
10:02:00D queues1m51s5m

idle never approaches 5m, so every request gets the lenient budget. A burst is absorbed without the grid ever changing state.

Grid jammed. A broken browser image hangs on startup, so A and B never finish and nothing is ever admitted again. With no fast admissions, idle only grows:

TimeEventidleBudget given
10:00:00A and B admitted, then hang0—
10:00:10C queues10s5m
10:05:10C refused, its 5m is up5m10s—
10:05:11D queues5m11s30s — saturated
10:05:41D refused, its 30s is up5m41s—

C still waited the full five minutes, and that is correct: until idle crosses -queue-timeout, a jammed grid is indistinguishable from a merely busy one. From D onwards every request fails in 30s, so CI reports a broken grid in half a minute instead of tying up a build slot for five.

This is CoDel, the controlled-delay algorithm from network queue management, applied to session slots rather than packets. -queue-timeout is CoDel’s interval and the fast threshold is its target, kept at the same 5% of the interval the original uses.

The decision is driven by how long ago something was served, not by how many requests are queued right now. Depth is misleading here: on a saturated grid every waiter eventually times out and leaves, so the queue keeps emptying and reads as idle exactly when things are worst. Time since the last fast admission has no such blind spot, which is why CoDel is built on it.

SituationResult
-disable-queue is setImmediate error, no waiting
X-WebSummoner-No-Wait (or legacy X-Selenoid-No-Wait) sentImmediate 429 Too Many Requests
The wait budget elapses429 Too Many Requests
The client disconnects firstNothing — the slot is released, no response needed
A slot frees firstThe session starts normally

Both 429 responses carry a Retry-After header, randomised over [1, -queue-timeout] seconds. The randomness matters: a fixed value puts every rejected client on the same second, so they all come back together and jam the grid again. Honour the header as sent rather than rounding it.

Both are 429. The hub logs them as [QUEUE_TIMEOUT] and [QUEUE_SHED], and /metrics counts them separately:

MetricMeans
websummoner_queue_congested1 while shedding — alert on this, not on rejection counts
websummoner_queue_rejected_total{reason="no_wait"}Client opted out of waiting, usually ggr
websummoner_queue_rejected_total{reason="timeout"}Slow grid, queue otherwise healthy — consider raising -limit
websummoner_queue_rejected_total{reason="shed"}Saturated grid — needs capacity, not patience