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

Open
#14,701 4 comments 0 reactions 0 assignees View on GitHub
package:vite-plugin
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.