Should FileSystemCache read back fallback-shell upgrades from disk? #97681
Replies: 6 comments 2 replies
|
Not intended — this is a set/get asymmetry in
Those don't mirror each other. With So the C is the same bug but worse than A. A is a re-render loop. C is a correctness problem: the build wrote a valid Minor: the Don't ship the empty Workaround: a custom Fix — make Gating on |
|
I reproduced this independently on Node 24.18.1 with the repository's locked Next 16.3.2 dependencies. The behavior matches all three scenarios:
The underlying asymmetry is still present at current canary commit I also tested the tempting one-line change: (!ctx.isRoutePPREnabled || data.postponed == null)It fixes scenario A: A2 and A3 become disk HITs. It does not fix scenario C; the regenerated This looks like it needs two regression tests around the writer:
Those distinguish restoring cache persistence from restoring entry coherence, which the one-line change does not yet achieve. |
|
Thanks for the independent repros. Should I file this as an issue? |
|
I don't think the runtime-upgraded entry is intended to be memory-only. Your repro looks like it exposes an asymmetric read/write contract in In v16.3.2, the disk-read path does this for an App Router page: if (
!ctx.isFallback &&
(!ctx.isRoutePPREnabled || meta?.postponed == null)
) {
rscData = await this.fs.readFile(
this.getFilePath(
`${key}${RSC_SUFFIX}`,
IncrementalCacheKind.APP_PAGE
)
)
} |
Deep Dive: `FileSystemCache` PPR Upgrade Architecture & Disk Resolution AsymmetryGreat reproduction and analysis of Here is the breakdown of why this occurs, what 1. What
|
|
I think this is the same issue as #98306. There’s a proposed fix in #98307 that makes set() write .rsc for fully static PPR entries too. It’s still a draft though, not merged. I’d try it on your scenario C, not just /unlisted. The PR’s test only covers a new entry, whereas yours catches an old build-time .rsc being left behind after regeneration. You can get HIT and still serve the wrong data, which is the part I’d want to check. After restarting, do both the HTML and RSC: 1 responses return the regenerated rendered-at, before the next revalidation? That restart case would be worth adding to the PR’s tests too. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Repo with sample code: https://github.com/skurekjakub/next-ppr-fallback-upgrade-disk-cache
The repro.sh script in the repo does the following:
next buildwithcacheComponents: trueandpartialPrefetching: true.app/[slug]/page.tsxhasgenerateStaticParamsreturningknownand reads its data in a'use cache'function withcacheLife('max')./unlisted. The response is the fallback shell (x-nextjs-postponed: 1). The background upgrade writes.next/server/app/unlisted.html,unlisted.metaandunlisted.segments/. There is nounlisted.rsc./unlistedagain.With the default
cacheMaxMemorySizethe second request in the same process is a HIT from the in-memory LRU. Disk is only consulted when the LRU does not have the page: after an eviction, after a restart, or with the memory tier disabled. In each of those cases the request goes through the fallback shell and a full render, and the upgrade writes the files again.I expected the upgraded entry to be served from disk whenever it is not in memory, the way build-prerendered pages are. The ISR guide says later visitors get the upgraded page from the cache, and the self-hosting guide describes that cache as memory plus disk. I also expected the RSC payload of a regenerated page to match its HTML.
Question: Is it intended that these entries live only in memory on
next start? If yes, what are the.htmland.segments/files written by the ISR upgrade for?Additional information
A prerendered page that regenerates at runtime has a related problem.
app/revalidating/[slug]/page.tsxusescacheLife({ revalidate: 20 }). The regeneration rewritesrevalidating/known.htmland.metaand leaves.rscand.segments/from the build. After a restart, a normal request returns the regenerated page and anRSC: 1request for the same URL returns the build-time page.In
file-system-cache.ts(v16.3.2),get()checks the in-memory LRU and reads the disk only on a miss (L109-L120).set()writes.rsconly when!ctx.isRoutePPREnabled(L401-L402).get()reads.rscwhen the route is PPR-enabled andmeta.postponedis null (L233-L240), inside a try/catch that returns null (L293-L294). A fallback-shell upgrade, or a regeneration with all data cached, produces a fully static entry with no postponed state, soset()skips the file thatget()then requires. The build writes.rscthrough its own export path, which is why build-prerendered pages read back.Provide environment information
Operating System:
Platform: linux
Arch: x64
Version: #1 SMP PREEMPT_DYNAMIC Fri Jun 5 01:12:21 UTC 2026
Available memory (MB): 31931
Available CPU cores: 20
Binaries:
Node: 24.18.0
npm: 12.0.1
Yarn: N/A
pnpm: N/A
Relevant Packages:
next: 16.3.2 // Latest available version is detected (16.3.2).
eslint-config-next: N/A
react: 19.2.8
react-dom: 19.2.8
typescript: 5.9.3
Next.js Config:
output: standalone
Example
https://github.com/skurekjakub/next-ppr-fallback-upgrade-disk-cache
All reactions