Summary
Running the full suite on Windows kills a vitest pool worker with a V8 compiler-zone OOM. It has reproduced across separate runs with a byte-identical native stack. The crash is not visible in the JSON report — a dying worker is not a test failure, so it appears in no testResults entry and vitest recycles the worker and continues. A run can therefore report an internally consistent pass/fail summary while a worker was in fact killed.
This is a Zone OOM, not a JS heap OOM, so --max-old-space-size does not reach it.
The crash
#
# Fatal process out of memory: Zone
#
----- Native stack trace -----
1: 00007FF6F95A495D node::MultiIsolatePlatform::DisposeIsolate+5341
2: 00007FF6FAA12006 v8::base::FatalOOM+54
3: 00007FF6FA29F5E5 v8::Function::NewInstance+245
4: 00007FF6F9C19576 v8::SnapshotCreator::GetIsolate+56566
5: 00007FF6FAE93F5A v8::platform::DelayedTaskQueue::Terminate+1698970
6: 00007FF6FAE9364B v8::platform::DelayedTaskQueue::Terminate+1696651
7: 00007FF6FAE92B0D v8::platform::DelayedTaskQueue::Terminate+1693773
8: 00007FF6FAE925E4 v8::platform::DelayedTaskQueue::Terminate+1692452
9: 00007FF6FAE91B71 v8::platform::DelayedTaskQueue::Terminate+1689777
10: 00007FF6FAE918CF v8::platform::DelayedTaskQueue::Terminate+1689103
11: 00007FF6FAC9F686 v8::base::DiyFp::Multiply+414934
12: 00007FF6FAC9E3A0 v8::base::DiyFp::Multiply+410096
13: 00007FF6FAA722EB v8::internal::compiler::CompilationDependencies::FieldTypeDependencyOffTheRecord+192907
14: 00007FF6F9912F56 DH_get0_engine+658470
15: 00007FF6F990E693 DH_get0_engine+639843
16: 00007FF6F990DF79 DH_get0_engine+638025
17: 00007FF6FAA8519F v8::platform::DefaultJobHandle::UpdatePriority+591
18: 00007FF6F95A081E node::GetNodeReport+90430
19: 00007FF6FA2F46D6 uv_thread_detach+150
20: 00007FF6FB55A033 v8::base::UnsignedDivisionByConstant<unsigned __int64>+2907267
21: 00007FF9B23DCCB7 BaseThreadInitThunk+23
22: 00007FF9B2E0AD6C RtlUserThreadStart+44
Frames 13 and 17 (internal::compiler::CompilationDependencies, platform::DefaultJobHandle::UpdatePriority) put this on a background optimizing-compiler job thread, which is why the exhausted arena is a compiler Zone rather than the JS heap.
The same stderr also carries, from tinypool:
Error: Worker exited unexpectedly
❯ ChildProcess.onUnexpectedExit node_modules/tinypool/dist/index.js:118:30
❯ ChildProcess.emit node:events:521:24
❯ ChildProcess._handle.onexit node:internal/child_process:295:12
Reproduction
Two full-suite runs on the same host, hours apart, on different commits, produced identical stacks. Both were vitest run --maxWorkers=8 --minWorkers=1 under Node, driven by automation that preserves each run's stderr.
The second occurrence is the more informative one. That run reported 4163 passed / 59 failed — a report with no structural sign of a crash — while a worker had been killed by this OOM. Of its 59 failures, 58 were timeouts and one was a known wall-clock assertion; none corresponded to the crash, because none could.
Why this is worth fixing separately from the timeout noise
I originally filed #1773 attributing this host's unstable failure counts to machine load. That attribution was not controlled and I have corrected it there. Relevant to this issue: the compiler-zone OOM recurred after antivirus exclusions were partially applied, so unlike the timeout class it is not plausibly a scanning artifact. It is a distinct failure mode that survived the intervention which removed the timeouts.
The reporting gap is the part I would most like addressed:
Environment
- Windows 11 Pro (10.0.26200), 32 logical cores, 128 GB RAM
- Node v26.8.1 (not Bun)
vitest run --maxWorkers=8 --minWorkers=1, JSON + default reporters
Possible directions
I have not diagnosed the allocation that exhausts the zone, so this is a report rather than a fix, but a few things would each help independently:
- Surfacing worker deaths in the JSON report, or at minimum a non-zero structured signal distinguishable from ordinary test failure, so a crashed run cannot read as healthy.
- Whether a smaller
maxWorkers or poolOptions.forks.maxForks on Windows changes reproduction — worth knowing, though note that serializing the suite did not change the timeout class on this host, so I would not assume it helps here either.
- Whether any test compiles or evaluates unusually large generated sources; a compiler-zone OOM points at optimizing compilation, not at retained heap.
Happy to attach the complete stderr from either run, and the JSON report from the run that looked clean.
Summary
Running the full suite on Windows kills a vitest pool worker with a V8 compiler-zone OOM. It has reproduced across separate runs with a byte-identical native stack. The crash is not visible in the JSON report — a dying worker is not a test failure, so it appears in no
testResultsentry and vitest recycles the worker and continues. A run can therefore report an internally consistent pass/fail summary while a worker was in fact killed.This is a Zone OOM, not a JS heap OOM, so
--max-old-space-sizedoes not reach it.The crash
Frames 13 and 17 (
internal::compiler::CompilationDependencies,platform::DefaultJobHandle::UpdatePriority) put this on a background optimizing-compiler job thread, which is why the exhausted arena is a compiler Zone rather than the JS heap.The same stderr also carries, from tinypool:
Reproduction
Two full-suite runs on the same host, hours apart, on different commits, produced identical stacks. Both were
vitest run --maxWorkers=8 --minWorkers=1under Node, driven by automation that preserves each run's stderr.The second occurrence is the more informative one. That run reported 4163 passed / 59 failed — a report with no structural sign of a crash — while a worker had been killed by this OOM. Of its 59 failures, 58 were timeouts and one was a known wall-clock assertion; none corresponded to the crash, because none could.
Why this is worth fixing separately from the timeout noise
I originally filed #1773 attributing this host's unstable failure counts to machine load. That attribution was not controlled and I have corrected it there. Relevant to this issue: the compiler-zone OOM recurred after antivirus exclusions were partially applied, so unlike the timeout class it is not plausibly a scanning artifact. It is a distinct failure mode that survived the intervention which removed the timeouts.
The reporting gap is the part I would most like addressed:
vitest.jsonalone and concluded it was clean apart from timeouts; the fatal crash was in stderr the whole time.success: true, 0 failed, 4396 passed) that still exits 1 — the exit code and the report disagree in both directions. Automation cannot currently trust either one alone.Environment
vitest run --maxWorkers=8 --minWorkers=1, JSON + default reportersPossible directions
I have not diagnosed the allocation that exhausts the zone, so this is a report rather than a fix, but a few things would each help independently:
maxWorkersorpoolOptions.forks.maxForkson Windows changes reproduction — worth knowing, though note that serializing the suite did not change the timeout class on this host, so I would not assume it helps here either.Happy to attach the complete stderr from either run, and the JSON report from the run that looked clean.