Vitest isolate: when turning it off speeds up CI and when it breaks your tests
Disabling Vitest isolate can cut your test suite time in half — or surface a dozen flaky tests you never knew you had. Here's the tradeoff, spelled out.
BuildPulse Team
July 27, 2026

The setting everyone finds when their Vitest suite gets slow
You've got a test suite that's crept up to eight minutes on CI. Someone opens the Vitest docs, spots isolate, reads that setting it to false makes things faster, flips it, and — genuinely — the suite drops to four minutes. Ship it.
Two days later three tests start failing intermittently. Not always. Not the same three. Rerun the job and they pass. Someone adds a --retry flag and moves on.
Congratulations: you traded runtime for a signal you can no longer trust. Let's talk about what vitest isolate actually does, why turning it off is often the right call, and how to do it without quietly poisoning your CI.
What Vitest isolate actually does
By default, Vitest runs each test file in its own isolated environment. Under the hood it uses a pool of workers (forks or threads), and with isolation on, each test file gets a fresh module registry and a clean global scope. Think of it as a new sandbox per file.
That isolation costs money. Spinning up a fresh environment for every file — re-evaluating modules, rebuilding the global scope — adds real overhead, especially when you have hundreds of small test files. The setup tax dwarfs the actual assertion time.
When you set isolate: false, Vitest reuses the same environment across files within a worker. Modules stay loaded. Globals persist. No teardown-and-rebuild between files.
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// default is true
isolate: false,
pool: 'threads',
},
})
You can also flip it from the CLI:
vitest run --no-isolate
The speedup is real. On suites with lots of files and light per-file logic, dropping isolation can cut wall-clock time by 30–50%. That's not a rounding error — that's a coffee break you get back on every push.
The catch: shared state doesn't clean up after itself
Here's the part the docs mention and everyone skips. When files share an environment, they share everything that lives in that environment. Module-level state. Global mocks. Timers. Registered event listeners. The lot.
Isolation was papering over a whole class of bugs in your tests. Turn it off and those bugs get a stage.
Concrete example. Say one file mutates a module-level singleton:
// config.ts
export const config = { featureFlags: { newCheckout: false } }
// checkout.test.ts
import { config } from './config'
test('enables new checkout', () => {
config.featureFlags.newCheckout = true
expect(renderCheckout()).toContain('New')
})
With isolation on, the next file gets a fresh config with newCheckout: false. With isolation off — and if that next file runs in the same worker — it inherits newCheckout: true. A test that asserts the default behavior now fails. Or passes, depending on which worker picked it up and in what order.
That last part is the killer. Vitest distributes files across workers, and the distribution isn't deterministic across runs. So the same leak produces a failure only when two specific files land in the same worker in a specific order. That's the textbook definition of a flaky test: passes and fails on identical code, no source change in sight.
The usual suspects for state leaks
When --no-isolate starts producing intermittent failures, it's almost always one of these:
- Global mocks that aren't reset.
vi.mockand spies persist. If you don'tvi.restoreAllMocks()in anafterEach, a mock from file A bleeds into file B. - Fake timers left running.
vi.useFakeTimers()without a matchingvi.useRealTimers()hands the next file a frozen clock. - Module-level mutable state. Singletons, caches, connection pools, in-memory stores that get written to and never reset.
- Global objects. Anything you attached to
globalThis,process.envmutations, monkey-patched built-ins. - Unremoved listeners.
process.on(...), event emitters, DOM listeners in jsdom that accumulate across files.
None of these are Vitest bugs. They're test-hygiene debts that isolation was subsidizing. isolate: false just calls the loan.
How to turn isolation off without regretting it
The move isn't "flip the flag and hope." It's "flip the flag, then earn it back."
Reset aggressively in global setup. Put teardown in a setup file so it applies everywhere, not just the files someone remembered to annotate:
// test-setup.ts
import { afterEach, vi } from 'vitest'
afterEach(() => {
vi.restoreAllMocks()
vi.clearAllTimers()
vi.useRealTimers()
})
// vitest.config.ts
export default defineConfig({
test: {
isolate: false,
setupFiles: ['./test-setup.ts'],
restoreMocks: true,
unstubEnvs: true,
unstubGlobals: true,
},
})
Those last three config flags do a lot of work: restoreMocks restores spies between tests, unstubEnvs resets vi.stubEnv calls, unstubGlobals undoes vi.stubGlobal. Turn them on before you turn isolation off.
Expose the ordering bug before it hides again. The insidious thing about leaks is they only fire on specific file orderings. Run with a shuffled sequence to smoke them out:
vitest run --no-isolate --sequence.shuffle
If your suite passes clean under --no-isolate on ten shuffled runs, your isolation debt is probably paid. If it doesn't, you've found the exact tests to fix — better to find them locally than watch them flake in production CI for a month.
Consider a middle ground. You don't have to make it binary. Keep isolation off for the fast, pure-logic files and on for the messy integration ones. Vitest lets you set isolate per project with workspaces, or you can split into two commands: one --no-isolate run for unit tests, one isolated run for the integration suite that touches shared resources.
// vitest.config.ts — projects with different isolation
export default defineConfig({
test: {
projects: [
{
extends: true,
test: { name: 'unit', include: ['src/**/*.unit.test.ts'], isolate: false },
},
{
extends: true,
test: { name: 'integration', include: ['src/**/*.int.test.ts'], isolate: true },
},
],
},
})
pool matters too
While you're in here: isolate interacts with pool. Vitest supports forks (separate processes) and threads (worker threads). Forks give you stronger isolation between workers because processes don't share memory the way threads do. If you're seeing leaks even with isolation on, or you have native modules that misbehave under threads, pool: 'forks' is worth a look — at some runtime cost.
The general performance ladder, fastest to safest:
threads + isolate: false -> fastest, most leak-prone
threads + isolate: true -> fast, safe within a file
forks + isolate: true -> slower, strongest isolation
Most teams land on threads + isolate: true as the default and only drop isolation once they've done the hygiene work above.
Speed you can't get back from a flag
Here's the uncomfortable truth about chasing isolate: false for CI speed. You can shave minutes by sharing worker state — but if the price is a suite that fails 1-in-20 for reasons no one can reproduce, you didn't make CI faster. You made it slower, because now every red build triggers a rerun, an investigation, and a Slack thread. Flaky tests are the most expensive kind of slow.
If your goal is genuinely faster pipelines, isolation is one lever, but the bigger ones usually live elsewhere: parallelism, caching, and the hardware underneath your jobs. We've written before about why your CI is slow and how to actually fix it, and if you're on GitHub Actions, faster runners do more for wall-clock time than any test config flag — without asking you to trade away isolation guarantees.
And once you've flipped --no-isolate, you need a way to know if it introduced flakiness rather than finding out from a frustrated engineer three weeks later. That's the whole point of tracking test outcomes across runs instead of eyeballing them: BuildPulse ingests your JUnit output and flags tests that pass and fail on the same commit, so a state leak from a shared worker shows up as data, not a hunch.
The short version
vitest isolate(on by default) gives each test file a fresh environment. Safe, but slower.isolate: falsereuses environments across files. Faster, but shared state leaks between files.- Before you disable it: turn on
restoreMocks,unstubEnvs,unstubGlobals, add a global teardown, and run with--sequence.shuffleto expose ordering bugs. - Use per-project config to keep isolation on for the tests that actually need it.
- If the flag buys you speed but costs you a trustworthy signal, it wasn't a win.
Turning off isolation is a fine optimization. Just don't let it be the reason you stop trusting your green checkmark.
Related posts