Describe the bug
In a large @vitest/browser-playwright suite, runs intermittently fail with a global unhandled rejection (no test in the stack — every test passes):
Error: route.fulfill: The object has been collected to prevent unbounded heap growth.
❯ …/@vitest/browser-playwright/dist/index.js
The provider serves modules (and vi.mock module mocks) via context.route(predicate, route => route.fulfill(...)). All test files in a session run through one persistent browser context on a single Playwright connection, and the Request/Route/Response dispatchers created per intercepted request are never disposed between files.
Playwright caps each dispatcher "bucket" at 10,000 (maxDispatchersForBucket in playwright-core, ?? 1e4, no public override) and, once exceeded, evicts the oldest to bound memory (DispatcherConnection.maybeDisposeStaleDispatchers). When a still-live module-serving Route later .fulfill()s a request whose Request dispatcher was evicted, sendMessageToServer throws because object._wasCollected is true — surfacing as route.fulfill: … collected to prevent unbounded heap growth.
The overflow is deterministic (it happens every run once the suite is large enough); the thrown error is probabilistic on top of it (only when an evicted Route/Request is the one later fulfilled), which is why it presents as a flake.
Evidence
Instrumenting playwright-core's dispatcher registry during a full run of a ~330-file browser-mode suite (one chromium instance):
- The
Request bucket climbs monotonically with no per-file reset: 1000 → 2000 → … → 10000, then evicts the oldest 1000 repeatedly (77 evictions in one full run; ~85k Request dispatchers created total).
- A
--shard=1/6 subset (54 files) alone reaches the 10,000 cap.
NODE_OPTIONS=--max-old-space-size does not help — the ceiling is dispatcher count, not V8 heap.
The provider (createContext(sessionId)) caches one context per session and reuses it for the whole run; reopening the page per file does not dispose the context's accumulated Request/Route/Response dispatchers.
Reproduction
Any browser-mode suite with enough files/vi.mock calls that cumulative intercepted requests exceed ~10,000 on one connection. Roughly: files × (module requests + mocks per file) > 10_000. In our repo it reproduces on ~10–15% of CI runs; instrumentation shows the bucket crossing 10k on every run.
Expected behavior
The provider should keep the per-connection dispatcher count bounded across files — e.g. dispose request/route dispatchers per file, recycle the browser context every N files, or otherwise release the module-serving Route/Request objects once a file completes — so a long suite never trips Playwright's GC eviction on live objects.
Suggested directions
- Dispose the per-file module-mock routes and their associated
Request/Response dispatchers when a test file finishes (context.unroute already runs, but the already-created Request dispatchers persist).
- Optionally recycle the
BrowserContext every N files under isolate: true.
- Or surface a config to raise Playwright's dispatcher cap (currently
setMaxDispatchersForTest is internal / not on playwright-core's public exports).
Workaround
We ship a pnpm patch raising playwright-core's per-bucket cap (env-tunable, default 1e6), which takes the observed peak from "10k + evicting" to "~85k with 0 evictions" and eliminates the failure. It's an interim — the real fix belongs in the provider's per-file dispatcher lifecycle.
Related issues (searched; this is not a duplicate)
Version / environment
vitest 4.1.8
@vitest/browser 4.1.8, @vitest/browser-playwright 4.1.8
playwright-core 1.61.0 (chromium, headless, one instances entry)
- Node 24.16, pnpm 11.9.0
Describe the bug
In a large
@vitest/browser-playwrightsuite, runs intermittently fail with a global unhandled rejection (no test in the stack — every test passes):The provider serves modules (and
vi.mockmodule mocks) viacontext.route(predicate, route => route.fulfill(...)). All test files in a session run through one persistent browser context on a single Playwright connection, and theRequest/Route/Responsedispatchers created per intercepted request are never disposed between files.Playwright caps each dispatcher "bucket" at 10,000 (
maxDispatchersForBucketinplaywright-core,?? 1e4, no public override) and, once exceeded, evicts the oldest to bound memory (DispatcherConnection.maybeDisposeStaleDispatchers). When a still-live module-servingRoutelater.fulfill()s a request whoseRequestdispatcher was evicted,sendMessageToServerthrows becauseobject._wasCollectedistrue— surfacing asroute.fulfill: … collected to prevent unbounded heap growth.The overflow is deterministic (it happens every run once the suite is large enough); the thrown error is probabilistic on top of it (only when an evicted
Route/Requestis the one later fulfilled), which is why it presents as a flake.Evidence
Instrumenting
playwright-core's dispatcher registry during a full run of a ~330-file browser-mode suite (one chromium instance):Requestbucket climbs monotonically with no per-file reset:1000 → 2000 → … → 10000, then evicts the oldest 1000 repeatedly (77 evictions in one full run; ~85kRequestdispatchers created total).--shard=1/6subset (54 files) alone reaches the 10,000 cap.NODE_OPTIONS=--max-old-space-sizedoes not help — the ceiling is dispatcher count, not V8 heap.The provider (
createContext(sessionId)) caches one context per session and reuses it for the whole run; reopening the page per file does not dispose the context's accumulatedRequest/Route/Responsedispatchers.Reproduction
Any browser-mode suite with enough files/
vi.mockcalls that cumulative intercepted requests exceed ~10,000 on one connection. Roughly:files × (module requests + mocks per file) > 10_000. In our repo it reproduces on ~10–15% of CI runs; instrumentation shows the bucket crossing 10k on every run.Expected behavior
The provider should keep the per-connection dispatcher count bounded across files — e.g. dispose request/route dispatchers per file, recycle the browser context every N files, or otherwise release the module-serving
Route/Requestobjects once a file completes — so a long suite never trips Playwright's GC eviction on live objects.Suggested directions
Request/Responsedispatchers when a test file finishes (context.unroutealready runs, but the already-createdRequestdispatchers persist).BrowserContextevery N files underisolate: true.setMaxDispatchersForTestis internal / not onplaywright-core's public exports).Workaround
We ship a pnpm patch raising
playwright-core's per-bucket cap (env-tunable, default1e6), which takes the observed peak from "10k + evicting" to "~85k with 0 evictions" and eliminates the failure. It's an interim — the real fix belongs in the provider's per-file dispatcher lifecycle.Related issues (searched; this is not a duplicate)
304 Not Modifiedscript responses (chromium 530892387) with avitest.configworkaround. This report is the Playwright dispatcher-count overflow (route.fulfill: object collected), independent of the 304 leak — it's about how manyRequest/Routedispatchers the provider holds on one connection, not per-request memory.Closing rpc while "fetch" was pending).Version / environment
vitest4.1.8@vitest/browser4.1.8,@vitest/browser-playwright4.1.8playwright-core1.61.0 (chromium, headless, oneinstancesentry)