Repository navigation
With isolate: false, each file's vi.mock factory is never released, so worker memory grows per file #11495
Description
Describe the bug
With isolate: false, every test file's vi.mock() factory stays reachable after the file finishes, together with everything the factory closes over. A worker that runs many files therefore grows by the size of each file's mock-factory scope, and on a large jsdom suite the worker eventually dies with Worker exited unexpectedly.
Mocking itself behaves correctly: each file sees its own mock. Only the memory is never released. vi.resetModules() at the start of each file does not change it. With isolate: true the heap stays flat.
In the reproduction, each of 30 test files mocks the same module with a factory that closes over ~10 MB. The heap measured at the start of each file, after a forced GC (--expose-gc), on one worker:
| config | heap used at file start (MB) |
|---|---|
isolate: false |
19, 28, 38, 47, 57 … 296 (+~9.6 MB per file) |
isolate: false + vi.resetModules() per file |
19, 28, 38, 47, 57 … 296 |
isolate: true |
19 for every file |
In a heap snapshot of a real suite, old copies are retained through EvaluatedModuleNode.meta.mockedModule.factory (VitestModuleRunner.evaluatedModules.idToModuleMap), and through mock configs whose vi.fn() keys are themselves reachable from those factories.
#11092 (shipped in 5.0, closing #9492) already stopped REGISTERED_MOCKS from pinning every vi.fn(). This is the remaining path that keeps earlier files alive when isolation is off; the last comments on #9492 describe the same "nothing gets released under isolate: false" symptom.
Reproduction
https://github.com/alexandrucojocaru-creatopy/vitest-isolate-false-mock-retention
npm install, then npm test; heap.log gets one line per file. RESET=1 npm test adds vi.resetModules(), and npm test -- --isolate is the control. The files, inline for convenience:
package.json
{ "name": "vitest-isolate-false-mock-retention", "private": true, "type": "module",
"scripts": { "test": "node --expose-gc node_modules/vitest/vitest.mjs run" },
"devDependencies": { "vitest": "5.0.3" } }vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: { isolate: false, pool: 'threads', maxWorkers: 1, fileParallelism: false, setupFiles: ['./heap.setup.ts'] },
});heap.setup.ts
import { appendFileSync } from 'node:fs';
import { beforeAll, vi } from 'vitest';
if (process.env.RESET === '1') vi.resetModules();
beforeAll(() => {
(globalThis as { gc?: () => void }).gc?.();
appendFileSync('heap.log', `${Math.round(process.memoryUsage().heapUsed / 1048576)}\n`);
});dep.ts
export const dep = (): string => 'real';test/t01.test.ts … test/t30.test.ts (identical apart from the name)
import { expect, test, vi } from 'vitest';
import { dep } from '../dep';
// ~10 MB that only this file's mock factory references.
const { payload } = vi.hoisted(() => ({ payload: Array.from({ length: 1_250_000 }, (_, i) => i) }));
vi.mock('../dep', () => ({ dep: () => String(payload.length) }));
test('sees its own mock', () => {
expect(dep()).toBe('1250000');
});Expected: the heap stays roughly flat once a file's mocks are no longer registered, as with isolate: true.
Actual: it grows by each file's factory scope for the life of the worker.
System Info
vitest 5.0.3 (also reproduces on 4.1.11)
node 24.11.1
macOS 15 (darwin 24.6.0), arm64
pool: threads
Used Package Manager
npm
Validations
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Make sure this is a Vitest issue and not a framework-specific issue.
- Check that this is a concrete bug.
- The provided reproduction is a minimal reproducible example of the bug.
Activity
sheremet-va commentedon Oct 5, 2026
I think it's fixed by #11197
alexandrucojocaru-creatopy commentedon Oct 5, 2026
Thanks for the quick look. I re-ran the reproduction against the pkg.pr.new build of the #11197 merge commit (
npm i vitest@https://pkg.pr.new/vitest@28ef93df22, with@vitest/spyand@vitest/mockerresolving to28ef93das well). It still grows by the same amount:28ef93d(#11197)28ef93dwith--isolateThe reproduction creates no
vi.fn()at all: the factory returns a plain function, so the spy registry isn't involved. What stays reachable is thevi.mockfactory closure itself, throughEvaluatedModuleNode.meta.mockedModule.factoryinevaluatedModules.idToModuleMap, along with everything that closure captures. Could you reopen it?