cloudflare / cloudflare/workers-sdk
[vite-plugin/miniflare] workerd heap grows on every server-file edit until OOM, then every request fails with `fetch failed` until manual restart
- Dominant language
- TypeScript
- Stars
- 4.5k
- Forks
- 1.5k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 183
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 MB` freeing zero bytes before the abort.
Detailed measurements (real app, vite-plugin 1.44.0 / miniflare 4.20260708.1)
All numbers are workerd RSS (`/proc//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
1. **Expose `v8Flags`** — #14702 adds a `MINIFLARE_WORKERD_V8_FLAGS` env passthrough (mirroring `MINIFLARE_WORKERD_AUTOGATES`, #8431). One line, unblocks anyone hitting this today.
2. **Restart workerd on crash** — #13045 already covers this; without it any workerd death bricks the dev server silently.
3. **Investigate module-runner heap retention** — the real fix; retention per edit suggests previous module generations are kept alive in the isolate.
4. When `dispatchFetch` fails after the runtime process exited, surface "workerd crashed — restart the dev server" instead of the bare undici `fetch failed`.
Workaround for anyone hitting this today
Patch `miniflare` (via `bun patch` / `patch-package`) to add one line in `#assembleConfig`'s return value:
```js
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`.
Contributor guide
Research direction
Start with the minimal reproduction and inspect Miniflare's #assembleConfig path, especially the missing v8Flags passthrough, then trace the module-runner server-file reload path for retained module generations. Compare behavior with the documented workaround and verify that a crashed workerd is detected and reported or recovered instead of returning repeated opaque fetch failed errors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript, vite
- Domain
- devtools, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100