AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Prerender Abort Reason Carries V8 Stack Frames, So A Retained AbortSignal Pins The Whole Render Graph (1.7 MB Vs 1 KB)

Next.js's createPrerenderInterruptedError creates a plain Error whose lazy stack accessor retains captured V8 call site frames until .stack is read. When that Error is used as AbortSignal.reason, any retained AbortSignal transitively pins the entire rendered page (html, segmentData, buffers, promises) via the frames' closures and contexts. Measured retention is ~1,756 KiB per signal, versus ~1 KiB when frames are stripped. The defect is latent: Next collects these signals normally, but userland/instrumentation commonly retains signals, turning a small leak into multi-GB memory pressure.

highConfidence 92%Next.jsAffected V16.3.0Affected V16.3.1-Canary.15

Origin Analysis

createPrerenderInterruptedError builds a new Error(message) without reading its .stack. V8 stores the Error's structured stack trace (an internal FixedArray of CallSiteInfo) until the stack getter is first invoked. When this Error is passed to controller.abort(error) and later retained via AbortSignal.reason, each retained frame's function/context keeps the entire render output alive. Nothing in production reads the frames, so they are pure overhead.
1. Clone https://github.com/devopsaptlife/nextjs-prerender-abort-retention 2. npm install && npm run build && npm run instrument 3. npm start & (starts standalone server) 4. curl -s localhost:3000/p/a localhost:3000/p/b localhost:3000/p/c > /dev/null 5. cat hits.log to confirm per-request dynamic rendering 6. Run node --expose-gc repro.mjs to measure retained memory: baseline shows ~160 MB pinned while signals alive; materialize and limit0 show ~0 MB (release).

Fixing Code Block

Edge Case Audit

Rollback is simple: remove the `error.stack = ...` assignment. The fix intentionally discards full stack frame details; any code that later reads `error.stack` for diagnostics will see only the synthetic string. In production this is acceptable because isPrerenderInterruptedError checks digest/name/message and create-error-handler early-returns on digest. However, if a monitoring tool or custom handler depends on full stack for prerender interruptions, it will lose that information. Node/V8 versions should be tested in CI: while the stack setter behavior is stable, an engine change could potentially alter the private slot clearing. Also note that the assignment must happen before any synchronous abort listener fires (it does) to avoid exposing an expensive reason. For non-production debugging, consider conditionally skipping the assignment or logging full stack before overwriting.

Ecosystem Topology