vercel / vercel/next.js

ISR revalidation retains RSC segment data via CachedParams WeakMap (external/arrayBuffers growth, amplified on Node 22)

Open
#96,533 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/udohsolomon/nextjs-isr-leak-repro

To Reproduce
  1. npm install && npm run build (bare app: one app/posts/[slug]/page.tsx with revalidate = 3600, 200 params from generateStaticParams, ~100 KB rendered body; instrumentation.ts logs process.memoryUsage() every 5s)
  2. npm run start > server.log 2>&1 &
  3. node drive.mjs server.log 25 - forces ISR re-renders of all 200 routes per round via the x-prerender-revalidate header and prints the server's memory line after each round
  4. Run once under Node 22.x and once under Node 24.x (fresh server each time)
Current vs. Expected behavior

Expected: repeated ISR re-renders of the same routes should not accumulate memory - each re-render replaces the cached entry, and the previous render's buffers become collectable and are collected.

Current: external/arrayBuffers memory produced by revalidation renders is retained across many rounds. On Node 22 the effect is much stronger than on Node 24 for the identical workload.

Minimal repro (200 routes x 25 forced-revalidation rounds, process.memoryUsage().arrayBuffers):

Node 22.22.3 Node 24.18.0
Behavior climbs to a 75-85 MB plateau sustained for 14 consecutive rounds (rounds 8-21) before a single full GC drops it to 27 MB, then rebuilds collected continuously; oscillates in a 24-42 MB band
RSS 271 -> 327 MB drift flat ~370-383 MB

Raw per-round numbers are committed in the repro repo (results-node22b.txt, results-node24.txt).

The magnitude scales with app size. In our real application (785 ISR routes, ~100 KB RSC payloads per page) the identical A/B held 190-240 MB under Node 22 vs 72-85 MB under Node 24, and in production (Next 16.2.7, 736 ISR pages revalidating hourly, Node 22.19, self-hosted next start) it accumulated to 1.16 GB of arrayBuffers over ~4 days with the JS heap steady at ~230 MB - the dominant share of container RSS.

Heap-snapshot evidence (V8 snapshot of the live next start process, Node 22): the retained Buffers are RSC segmentData/flightData bytes, and every sampled retainer chain roots through the same path:

Buffer (segmentData bytes)
  <- Map property:segmentData
  <- context closures
  <- property:onError Object
  <- <symbol kResourceStore> Promise        (AsyncLocalStorage store captured by the cached promise)
  <- WeakMap entry                          (CachedParams, module scope)
  <- closure createPrerenderParamsForClientSegment
     (next/dist/server/request/params.js)

The promise cached in the module-scope CachedParams WeakMap (packages/next/src/server/request/params.ts) captures the render's AsyncLocalStorage context; through the work store's onError, that roots the render's RSC segment buffers for as long as the params key object stays alive - so every revalidation adds another retained render until a full GC manages to sever what is collectable. With a typical self-hosted setup (no --max-old-space-size, small working heap vs the ~2 GB default ceiling), full GCs are rare enough that this accumulates for days.

Checked against 16.2.12 - the CachedParams path is unchanged and 16.2.8-16.2.12 contain no related fix.

Provide environment information
Operating System:
  Platform: linux
  Arch: x64
  Version: WSL2 (kernel 6.6) and Debian (Railway/Nixpacks container) - both reproduce
  Available memory (MB): 31821
  Available CPU cores: 22
Binaries:
  Node: 22.22.3 (strong retention) / 24.18.0 (much weaker) - production hit it on 22.19.0
  npm: 10.9.8
Relevant Packages:
  next: 16.2.12 (repro; also observed on 16.2.7 in production)
  react: 19.2.3
  react-dom: 19.2.3
  typescript: 6.0.3
Next.js Config:
  output: N/A (plain `next start`)
Which area(s) are affected?

Incremental Static Regeneration (ISR), Runtime

Which stage(s) are affected?

next start (local), Other (self-hosted production)

Additional context

Workarounds that contain it for us in production: run Node 24, and set --max-old-space-size low enough (e.g. 768 MB for a ~230 MB heap) that full GCs run regularly, which promptly collects the collectable share of the external memory. With those two changes our frontend went from grinding to 2.2+ GB RSS over days to a flat ~0.5 GB.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/next/src/server/request/params.ts, especially the module-scope CachedParams path and createPrerenderParamsForClientSegment. Run the linked reproduction under Node 22 and Node 24, then inspect whether repeated ISR revalidation retains RSC segmentData buffers through the reported AsyncLocalStorage chain. Done means the repeated rounds no longer accumulate arrayBuffers, with the observed behavior verified against the reproduction.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, node.js, react, typescript
Domain
backend, performance, web-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.