nextjs - 💡(How to fix) Fix Native V8 context leak in load-manifest.external.ts: per-page client-reference-manifest evaluation accumulates contexts until OOM

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

In production Next.js 16.2.6 standalone, RSS climbs linearly with the number of distinct app-router pages served, reaching OOM on apps with hundreds of pages. The leak is attributable to vm.runInNewContext in packages/next/src/server/load-manifest.external.ts. Each page's *_client-reference-manifest.js is evaluated once via runInNewContext, which allocates a fresh V8 native context (~3 to 4 MB residual RSS each) retained for the process lifetime by the manifest cache.

Root Cause

In production Next.js 16.2.6 standalone, RSS climbs linearly with the number of distinct app-router pages served, reaching OOM on apps with hundreds of pages. The leak is attributable to vm.runInNewContext in packages/next/src/server/load-manifest.external.ts. Each page's *_client-reference-manifest.js is evaluated once via runInNewContext, which allocates a fresh V8 native context (~3 to 4 MB residual RSS each) retained for the process lifetime by the manifest cache.

Code Example

T1 (same PID): uptime  5 min, RSS 424 MB, nativeContextCount 28
T2 (same PID): uptime 14 min, RSS 511 MB, nativeContextCount 51

---

let contextObject = {
  process: { env: { NEXT_DEPLOYMENT_ID: process.env.NEXT_DEPLOYMENT_ID } },
}
runInNewContext(content, contextObject)

---

globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {};
globalThis.__RSC_MANIFEST["/_not-found/page"] = { ... };
RAW_BUFFERClick to expand / collapse

Summary

In production Next.js 16.2.6 standalone, RSS climbs linearly with the number of distinct app-router pages served, reaching OOM on apps with hundreds of pages. The leak is attributable to vm.runInNewContext in packages/next/src/server/load-manifest.external.ts. Each page's *_client-reference-manifest.js is evaluated once via runInNewContext, which allocates a fresh V8 native context (~3 to 4 MB residual RSS each) retained for the process lifetime by the manifest cache.

Reproduction

  • Next.js 16.2.6, output: 'standalone', Node 24.15.0
  • App router with ~360 prerendered pages
  • Sustained crawler traffic that walks distinct pages
  • 1 GB Fly machine

Result: RSS grows ~10 MB/min, OOM after roughly 30 to 60 minutes uptime. Restart, repeat.

Diagnostic data

Captured via kill -USR1 <pid> + CDP Runtime.evaluate on the live process:

T1 (same PID): uptime  5 min, RSS 424 MB, nativeContextCount 28
T2 (same PID): uptime 14 min, RSS 511 MB, nativeContextCount 51

Same process, +9 min, +23 native contexts, +87 MB RSS. ~3.8 MB RSS per context.

Forced major GC via HeapProfiler.collectGarbage reclaims ~80 MB of heap but does not reduce nativeContextCount. Contexts are pinned by the load-manifest sharedCache Map, which retains the contextObject returned from each evalManifest call. The contextObject is the manifest's globalThis, so V8 keeps the entire native context alive.

Enumerating contexts via CDP Runtime.executionContextCreated shows they are all named "VM Context N" with barebones globals (V8 intrinsics only, no Edge runtime APIs). This rules out the Edge sandbox path and points at vm.runInNewContext in load-manifest.external.ts as the only producer.

Code location

packages/next/src/server/load-manifest.external.ts in evalManifest:

let contextObject = {
  process: { env: { NEXT_DEPLOYMENT_ID: process.env.NEXT_DEPLOYMENT_ID } },
}
runInNewContext(content, contextObject)

Manifest content (page_client-reference-manifest.js) only does:

globalThis.__RSC_MANIFEST = globalThis.__RSC_MANIFEST || {};
globalThis.__RSC_MANIFEST["/_not-found/page"] = { ... };

No prototype mutation, no this access, no eval, no implicit globals. The full V8 context isolation provided by runInNewContext is unused by the actual manifest content, but the cost is paid per page.

Projection

~360 pages × ~3.8 MB ≈ 1.4 GB ceiling, before the fetch cache leak under investigation in #85914 / #90433 adds anything.

Distinction from other reports

The reported memory leaks in #85914, #90433, #88603 all point at the fetch response cache. This is a different, additive source. Heap analysis on a process with both leaks active should show:

  • fetch cache leak: string and object accumulation referenced from cacheController
  • this leak: rising nativeContextCount and per-context globalThis.__RSC_MANIFEST retention

The two compound; apps with many pages will hit this one first.

Proposed fix

Replace runInNewContext with a Function wrapper that shadows globalThis / self / process via parameters. Same sandbox semantics for current manifest content, no V8 context allocation. PR follows.

Environment

  • Next.js: 16.2.6
  • Node: 24.15.0
  • Output: standalone
  • Container: 1 GB / 1 vCPU

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING