Vitest "pool" Threads vs Forks - Worker Crashes & Native Module Errors
Vitest runs tests in a worker pool that defaults to threads (worker_threads). Native addons and code that mutates process-global state can crash or misbehave under threads; switching the pool to forks (child processes) isolates them.
What this error means
A suite crashes with "Module did not self-register," a segfault, or "Terminating worker thread" - only in Vitest, not when the same code runs under Node directly. It often appears after adding a native dependency or under the default thread pool.
Error: Module did not self-register: '.../node_modules/better-sqlite3/build/Release/better_sqlite3.node'
❯ Worker terminated due to reaching memory limit or native crash
(pool: 'threads')Diagnose it: flake, environment, or genuine failure?
Before debugging the assertion, establish whether the test is deterministic. A test that fails only in CI is usually order-dependent, time-dependent, or racing something, and fixing the assertion will not help.
# does it fail in isolation?
npx vitest run path/to/file.test.ts
# is it order dependent? run the suite in a random order twice
npx vitest run --sequence.shuffle
# is it a race? run the same file repeatedly
for i in $(seq 1 20); do npx vitest run path/to/file.test.ts || break; doneCommon causes
Native addon loaded in a worker thread
A native N-API addon (better-sqlite3, canvas, bcrypt) may not support being loaded into multiple worker threads, and self-registration fails or crashes under the threads pool.
Process-global state shared across threads
Code that relies on per-process globals (some singletons, certain mocks) behaves incorrectly when many threads share one process. Forks give each test file its own process.
How to fix it
Switch the pool to forks
Run each test file in a child process instead of a worker thread.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { pool: 'forks', poolOptions: { forks: { singleFork: false } } },
});Tune isolation and concurrency
- Use
poolOptions.forks.singleFork: truefor code that must share one process. - Cap
maxForks/minForks(ormaxThreads) to control memory on big runners. - Keep
threadsfor pure-JS suites where it is faster; only move offending files to forks.
CI-only causes worth ruling out
- Runners have fewer cores than a laptop, so timing-sensitive tests that pass locally fail under contention.
- No TTY and a different locale or timezone. Snapshot tests containing formatted dates or numbers are the usual casualty; pin
TZandLANGin the job. - Parallel workers sharing a database, a port, or a temp directory. Give each worker its own namespace.
- Default timeouts calibrated on a fast machine. A cold runner is slower on first execution, especially before any cache warms.
How to prevent it
- Use the
forkspool for suites that load native addons. - Pin pool options in config so CI and local match.
- Avoid process-global singletons in code under test.