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

Open

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.

Activity

  1. changed the title [-][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[/+] on Jul 15, 2026
  2. workers-devprod commented on Jul 15, 2026

    @workers-devprod
    Contributor

    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 opaque fetch failed until the dev server is manually restarted.

    Classification

    Findings

    • Created: not available in prefetched data
    • Author: Sipixer (Sylvain Rougié)
    • Version: @cloudflare/vite-plugin@1.45.0, miniflare@4.20260710.0
    • Labels: not available in prefetched data
    • Comments: not available in prefetched data

    Note: The context.json body is a single very long line; the label/comment/date/state metadata that follows the body is not readable with the available read/grep tools (both truncate long lines at 2000 chars). Fields marked "not available" could not be extracted.

    Analysis

    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 v8Flags in 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 to fetch failed.

    Suggested Labels: (none applied — repo label existence could not be confirmed from the prefetched data)

    Suggested Comment

    Thanks for the extremely thorough write-up and the framework-free reproduction — this is really helpful. To make sure we route this correctly: we'll treat this as two linked problems — the per-edit heap growth during module-graph re-evaluation in @cloudflare/vite-plugin, and Miniflare not exposing a way to raise the workerd heap limit (your #14702). We'll take a look at the repro and review the mitigation PR. If you have any additional data (e.g. heap snapshots before/after a full GC across several edits), feel free to attach it here.

    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": []
    }
    
  3. Sipixer commented on Jul 17, 2026

    @Sipixer
    ContributorAuthor

    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.code on the module node, once by V8 itself (the UnsafeEval.eval Script 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 — getModuleSourceMapById extracts the inline map from meta.code and caches the decoded result on mod.map (with an early return). So the map is extracted eagerly right after evaluation, and both retained copies of the source become dead weight:

    1. in cachedRequest's finally: getModuleSourceMapById(mod.id), then meta.code = "" — later stack traces hit the cached mod.map
    2. delete sourcesContent from the decoded map — only used for code excerpts in error overlays; file:line mapping needs only the mappings
    3. strip the //# sourceMappingURL=... comment before UnsafeEval.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 runtime
    Original Prototype
    Live baseline (repro, 20 modules) 21.9 MB 9.2 MB (−58%)
    Heap churn after 30 edits (no forced GC) 215.7 MB 68.2 MB (−68%)
    Real app workerd RSS after first render ~850 MB 524 MB (−38%)
    Error.stack mapping (/throw route) src/mod0.ts:702:75 identical

    Known 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-side err.stack mapping 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.

  4. petebacondarwin commented on Jul 22, 2026

    @petebacondarwin
    Contributor

    I dug into this a bit today and came up with this issue with workerd: cloudflare/workerd#6894

  5. petebacondarwin commented on Jul 23, 2026

    @petebacondarwin
    Contributor

    We have landed a restart on OOM errors, which will mitigate this a little: #14792

  6. moved this from Untriaged to Backlog in workers-sdkon Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions