Repository navigation
[vite-plugin/miniflare] workerd heap grows on every server-file edit until OOM, then every request fails with fetch failed until manual restart #14701
Description
TL;DR: During dev with @cloudflare/vite-plugin, the workerd isolate's heap grows on every server-file edit (module graph re-evaluation). Large apps reach V8's default ~1.4 GB heap limit within a normal editing session; workerd then aborts and — since Miniflare has no crash recovery (#13045) — every request returns the opaque fetch failed until the dev server is manually restarted. There is currently no way to raise the limit: the workerd config supports v8Flags but Miniflare never populates it, and NODE_OPTIONS only affects the Node process. Minimal framework-free repro below; one-line mitigation in #14702.
What happens
V8 fatal error; location = Reached heap limit; message = : allocation failed: JavaScript heap out of memory
*** Received signal #6: Aborted
stack: .../node_modules/@cloudflare/workerd-linux-64/bin/workerd@...
then, for every subsequent request:
fetch failed
at Object.processResponse (node_modules/miniflare/node_modules/undici/lib/web/fetch/index.js:237:16)
at async _Miniflare.dispatchFetch (node_modules/miniflare/dist/src/index.js:91788:22)
at async @cloudflare/vite-plugin/dist/index.mjs:37094:19
We believe this is widely misdiagnosed: the V8 fatal error is printed once, then buried under thousands of fetch failed traces that look like a network problem (or like the fixed #13013). We hit it daily on Windows 11 and Linux before diagnosing it.
Minimal reproduction
https://github.com/sipixer/vite-plugin-cloudflare-heap-repro — plain fetch handler + generated modules importing one hub.ts, no framework. Append one line to hub.ts, request, measure workerd RSS, repeat. Reproduces on latest (@cloudflare/vite-plugin@1.45.0 / miniflare@4.20260710.0):
- Each edit transiently costs ~5× the re-evaluated source in heap (~50 MB for a 10 MB graph).
- Post-full-GC floor climbs (372 → 866 MB over 55 edits); with a 22 MB graph, repeated runaway phases where 6–7 consecutive edits reclaim nothing (880 → 1540 MB RSS).
- Our real app (TanStack Start SSR, ~850 MB baseline after first render) crosses the limit in 30–60 edits: the crash log shows back-to-back last-resort
Mark-Compact 1401.2 → 1401.2 MBfreeing zero bytes before the abort.
Detailed measurements (real app, vite-plugin 1.44.0 / miniflare 4.20260708.1)
All numbers are workerd RSS (/proc/<pid>/status VmRSS), requests are GET / (SSR):
| Scenario | Result |
|---|---|
| Baseline after first SSR render | ~850 MB |
| 10 requests, no edits | +2 MB total |
| 10 server-file edits (append one comment line to a widely-imported module, then 1 request each) | +10–12 MB per edit (846 → 928 MB) |
| Organic crash (real editing session) | Reached heap limit at V8 heap 1401 MB, ~31 min after startup |
| After the crash | 3571 consecutive fetch failed (500 in ~2 ms); only a full dev-server restart recovers |
Positive control: injecting v8Flags: ["--max-old-space-size=256"] into the generated config makes workerd OOM at exactly 255.9 MB on first render — the limit is the V8 default, and raising it works: with --max-old-space-size=4096, 70 edit cycles + 238 concurrent requests completed with zero failures where the default config died after 30–60 edits.
Plain .ts edits do not restart workerd (in-place HMR through the runner Durable Object), so this is isolate heap growth, not process churn. Environment: wrangler 4.77.0, TanStack Start 1.167.8, rolldown-vite 7.3.1, Node 24, reproduced on Debian 13 and Windows 11.
Suggested fixes
- Expose
v8Flags— [miniflare] Support workerd V8 flags via MINIFLARE_WORKERD_V8_FLAGS environment variable #14702 adds aMINIFLARE_WORKERD_V8_FLAGSenv passthrough (mirroringMINIFLARE_WORKERD_AUTOGATES, Support autogates viaMINIFLARE_WORKERD_AUTOGATESenv var #8431). One line, unblocks anyone hitting this today. - Restart workerd on crash — [miniflare] Restart workerd process on crash after successful startup #13045 already covers this; without it any workerd death bricks the dev server silently.
- Investigate module-runner heap retention — the real fix; retention per edit suggests previous module generations are kept alive in the isolate.
- When
dispatchFetchfails after the runtime process exited, surface "workerd crashed — restart the dev server" instead of the bare undicifetch failed.
Workaround for anyone hitting this today
Patch miniflare (via bun patch / patch-package) to add one line in #assembleConfig's return value:
v8Flags: process.env.MINIFLARE_WORKERD_V8_FLAGS ? process.env.MINIFLARE_WORKERD_V8_FLAGS.split(" ") : [],then run dev with MINIFLARE_WORKERD_V8_FLAGS=--max-old-space-size=4096.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog
Activity
[-][vite-plugin/miniflare] workerd hits V8's default heap limit during long dev sessions (heap grows on every server-file edit), then every request fails with `fetch failed` until the dev server is manually restarted[/-][+][vite-plugin/miniflare] workerd heap grows on every server-file edit until OOM, then every request fails with `fetch failed` until manual restart[/+]workers-devprod commentedon Jul 15, 2026
Note
This is an automated, advisory triage report generated by workers-devprod. It is not an official maintainer response — a maintainer will follow up.
🤖 Automated triage report
Issue Triage: #14701
Summary
During Vite dev with
@cloudflare/vite-plugin, the workerd isolate's heap grows on every server-file edit (module-graph re-evaluation), eventually hitting V8's default ~1.4 GB limit; workerd aborts and — with no Miniflare crash recovery — every subsequent request returns an opaquefetch faileduntil the dev server is manually restarted.Classification
packages/vite-plugin-cloudflare(andpackages/miniflare)Findings
@cloudflare/vite-plugin@1.45.0,miniflare@4.20260710.0Analysis
@cloudflare/vite-plugindev-server module-graph re-evaluation feeding the Miniflare/workerd isolate. Stack traces reference@cloudflare/vite-plugin/dist/index.mjsandminiflare/dist/src/index.js. This is not a workerd runtime bug per se, but a dev-loop memory-growth problem in the plugin + a Miniflare gap (no way to setv8Flags).fetch failederrors. The reporter notes it is easily misdiagnosed as a network problem or as the already-fixed POST requests returning non-2xx status crash with "fetch failed" #13013, and that they hit it daily on both Windows 11 and Linux.v8Flags). This is an actionable, well-scoped report with an existing candidate fix.v8Flagseven though the workerd config supports it. [miniflare] Support workerd V8 flags via MINIFLARE_WORKERD_V8_FLAGS environment variable #14702 addresses the second.Recommendation
Status: NEEDS VERIFICATION
Reasoning: This is a high-quality, well-diagnosed bug with a minimal framework-free reproduction on the latest versions and an existing candidate mitigation PR (#14702). It clearly belongs in workers-sdk, so it should stay open; a maintainer should verify the repro and review the proposed fix.
Action: A maintainer should run the provided reproduction to confirm the per-edit heap growth, review PR #14702 (populate
v8Flagsin Miniflare so the heap limit can be raised), and separately investigate the root-cause leak in the vite-plugin module-graph re-evaluation. Coordinate with #13045 (crash recovery) so a workerd abort no longer degrades silently tofetch failed.Suggested Labels: (none applied — repo label existence could not be confirmed from the prefetched data)
Suggested Comment
Structured summary (JSON)
{ "issueNumber": 14701, "title": "workerd isolate heap grows on every dev edit until V8 heap limit, then all requests return opaque \"fetch failed\"", "githubUrl": "https://github.com/cloudflare/workers-sdk/issues/14701", "recommendation": "NEEDS VERIFICATION", "difficulty": "medium", "reasoning": "High-quality bug with a minimal framework-free reproduction on the latest versions and an existing candidate mitigation PR (#14702). It clearly belongs in workers-sdk and should stay open for verification.", "suggestedAction": "Run the provided reproduction to confirm per-edit heap growth, review PR #14702 (populate v8Flags in Miniflare), and investigate the root-cause leak in the vite-plugin module-graph re-evaluation; coordinate with #13045 for crash recovery.", "hasSuggestedComment": true, "suggestedLabels": [] }Sipixer commentedon Jul 17, 2026
I profiled the minimal repro through workerd's inspector (heap snapshots force a full GC → true live set), then prototyped a fix. Everything below is reproducible from the repro repo: https://github.com/Sipixer/vite-plugin-cloudflare-heap-repro (profiling scripts +
prototype.patch).Main finding: there is no classic leak. After 30+ edits the live set only grows by ~0.8 MB. The pressure comes from each module's transformed source (with its inline base64 sourcemap) being retained for the lifetime of the graph — twice: once as
fetchedModule.codeon the module node, once by V8 itself (theUnsafeEval.evalScript pins its source string while the module's functions are alive). On top of that, re-evaluation waves transiently spike ~5× the re-evaluated source, and on a large app one wave crosses the fixed ~1.4 GB ceiling mid-flight — where everything genuinely is reachable, hence the last-resort GC freeing zero bytes in my crash log.Prototype fix — 13 lines, confined to the plugin's bundled
runner-worker(no upstream Vite changes). The invariant making it safe: once a module has been evaluated, the only thing that ever reads its transformed source again is stack-trace sourcemapping —getModuleSourceMapByIdextracts the inline map frommeta.codeand caches the decoded result onmod.map(with an early return). So the map is extracted eagerly right after evaluation, and both retained copies of the source become dead weight:cachedRequest'sfinally:getModuleSourceMapById(mod.id), thenmeta.code = ""— later stack traces hit the cachedmod.mapsourcesContentfrom the decoded map — only used for code excerpts in error overlays; file:line mapping needs only the mappings//# sourceMappingURL=...comment beforeUnsafeEval.eval— V8 pins the full eval'd source while the module's functions are alive, and the base64 map (~half the string) is never read back from V8 at runtimeError.stackmapping (/throwroute)src/mod0.ts:702:75Known trade-offs: error overlays lose original-source excerpts, and DevTools' Sources panel shows transformed code for SSR modules (both tunable — keep
sourcesContent, or only strip the inline map when no debugger is attached). Runner-sideerr.stackmapping is unaffected.Happy to turn this into a PR if you'd take it — and #14702/#13045 still matter independently: even with the prototype, a big enough app legitimately exceeds a fixed ceiling, and any workerd crash currently bricks the dev server silently.
petebacondarwin commentedon Jul 22, 2026
I dug into this a bit today and came up with this issue with workerd: cloudflare/workerd#6894
petebacondarwin commentedon Jul 23, 2026
We have landed a restart on OOM errors, which will mitigate this a little: #14792
fix(dev): raise workerd heap ceiling to survive HMR leak
fix(ci): segment the launch-load drive to bound the dev runtime's ret…