Description
[lib/command/workers/runTests.js:132-137] staggers every worker's startup with a hardcoded, unconditional delay before any initialization (config load, container init, test file loading):
// Add staggered delay at the very start to prevent resource conflicts
// Longer delay for browser initialization conflicts
const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay))
}
This was introduced in #5370 (bundled together with the uncaughtException/unhandledRejection handlers, hang-detection timeouts, and the mocha-state-pollution fix from the same commit).
Current behavior
The delay scales linearly and without bound with worker count, and applies to the entire worker bootstrap, not just the actual browser launch it's meant to protect. With run-workers 25:
| Worker # |
Delay before init starts |
| 1 |
0s |
| 10 |
18s |
| 25 |
48s |
That's 48 seconds of pure dead time before the last worker even starts loading its config, on every single CI run — and it only gets worse as worker count grows (50 workers → 98s, etc). There's no config option to tune or disable it.
Expected behavior / suggestion
We understand the motivation — launching many headless browsers at the exact same instant can cause resource-contention crashes — but the fix could avoid the unbounded linear cost. A few options, roughly in order of effort:
Cap the delay, e.g. Math.min((workerIndex - 1) * 2000, someMax), so it stops growing past a fixed ceiling regardless of worker count.
Batch workers into waves instead of staggering every single one individually, e.g. Math.floor((workerIndex - 1) / batchSize) * 2000 — limits how many workers launch a browser at once without making later workers wait proportionally to the total worker count.
Move the stagger to wrap only the actual browser-launch step (in the Playwright helper) instead of the whole worker init, so config/container/test-file loading for all workers can proceed immediately and in parallel — only the resource-contentious part (browser spawn) gets serialized.
Replace the fixed delay with an adaptive limiter — e.g. workers signal the parent process (which already receives messages from every worker) before launching a browser, and the parent gates concurrent launches to some max N, releasing the next queued worker once an active one signals it has finished launching — instead of guessing a fixed per-worker time.
Ideally this would also become configurable (env var or config option) rather than hardcoded, so projects with many workers or slower/faster CI hardware aren't stuck with one fixed assumption.
Happy to put together a PR for one of the above if there's appetite for it — let us know which direction you'd prefer.
Environment
CodeceptJS: 4.1.0 (confirmed the same code is present in 4.0.8 and 4.0.9 as well; not present in 3.7.7)
run-workers (reproduces regardless of --by distribution mode, since the delay runs before mode branching)
Description
[lib/command/workers/runTests.js:132-137] staggers every worker's startup with a hardcoded, unconditional delay before any initialization (config load, container init, test file loading):
This was introduced in #5370 (bundled together with the uncaughtException/unhandledRejection handlers, hang-detection timeouts, and the mocha-state-pollution fix from the same commit).
Current behavior
The delay scales linearly and without bound with worker count, and applies to the entire worker bootstrap, not just the actual browser launch it's meant to protect. With run-workers 25:
That's 48 seconds of pure dead time before the last worker even starts loading its config, on every single CI run — and it only gets worse as worker count grows (50 workers → 98s, etc). There's no config option to tune or disable it.
Expected behavior / suggestion
We understand the motivation — launching many headless browsers at the exact same instant can cause resource-contention crashes — but the fix could avoid the unbounded linear cost. A few options, roughly in order of effort:
Cap the delay, e.g. Math.min((workerIndex - 1) * 2000, someMax), so it stops growing past a fixed ceiling regardless of worker count.
Batch workers into waves instead of staggering every single one individually, e.g. Math.floor((workerIndex - 1) / batchSize) * 2000 — limits how many workers launch a browser at once without making later workers wait proportionally to the total worker count.
Move the stagger to wrap only the actual browser-launch step (in the Playwright helper) instead of the whole worker init, so config/container/test-file loading for all workers can proceed immediately and in parallel — only the resource-contentious part (browser spawn) gets serialized.
Replace the fixed delay with an adaptive limiter — e.g. workers signal the parent process (which already receives messages from every worker) before launching a browser, and the parent gates concurrent launches to some max N, releasing the next queued worker once an active one signals it has finished launching — instead of guessing a fixed per-worker time.
Ideally this would also become configurable (env var or config option) rather than hardcoded, so projects with many workers or slower/faster CI hardware aren't stuck with one fixed assumption.
Happy to put together a PR for one of the above if there's appetite for it — let us know which direction you'd prefer.
Environment
CodeceptJS: 4.1.0 (confirmed the same code is present in 4.0.8 and 4.0.9 as well; not present in 3.7.7)
run-workers (reproduces regardless of --by distribution mode, since the delay runs before mode branching)