Skip to content

[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

@Sipixer

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/<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

  1. Expose v8Flags[miniflare] Support workerd V8 flags via MINIFLARE_WORKERD_V8_FLAGS environment variable #14702 adds a MINIFLARE_WORKERD_V8_FLAGS env passthrough (mirroring MINIFLARE_WORKERD_AUTOGATES, Support autogates via MINIFLARE_WORKERD_AUTOGATES env var #8431). One line, unblocks anyone hitting this today.
  2. 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.
  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:

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

No one assigned

    Labels

    package:vite-pluginRelating to the `@cloudflare/vite-plugin` package

    Type

    No type

    Projects

    Status
    Untriaged

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions