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')Common 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.
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.