Skip to content
Latchkey

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.

Vitest output
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.

Terminal
# 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; done

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
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: { pool: 'forks', poolOptions: { forks: { singleFork: false } } },
});

Tune isolation and concurrency

  1. Use poolOptions.forks.singleFork: true for code that must share one process.
  2. Cap maxForks/minForks (or maxThreads) to control memory on big runners.
  3. Keep threads for 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 TZ and LANG in 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 forks pool for suites that load native addons.
  • Pin pool options in config so CI and local match.
  • Avoid process-global singletons in code under test.

Frequently asked questions

What causes Vitest "pool" threads vs forks?
There are 2 common causes: native addon loaded in a worker thread and process-global state shared across threads. 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.
How do I fix Vitest "pool" threads vs forks?
There are 2 fixes depending on which cause you have: switch the pool to forks and tune isolation and concurrency. Work through them in order, since the first is the most common.
What does Vitest "pool" threads vs forks actually mean?
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.
How do I stop Vitest "pool" threads vs forks happening again?
Use the forks pool for suites that load native addons. The prevention section lists 3 changes that keep it from recurring.

Related guides

References

Not every red build is your code. Latchkey repairs the ones that are not, on the runner. Start free → 30-day trial · No credit card