ISR revalidation retains RSC segment data via CachedParams WeakMap (external/arrayBuffers growth, amplified on Node 22)
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
npm install && npm run build(bare app: oneapp/posts/[slug]/page.tsxwithrevalidate = 3600, 200 params fromgenerateStaticParams, ~100 KB rendered body;instrumentation.tslogsprocess.memoryUsage()every 5s)npm run start > server.log 2>&1 &node drive.mjs server.log 25- forces ISR re-renders of all 200 routes per round via thex-prerender-revalidateheader and prints the server's memory line after each round- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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