Runtime-rendered fully static pages on PPR routes are stored without .rsc and are never read back from the file-system cache (MISS forever)
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/Shtokarev/next-ppr-runtime-rsc-repro
To Reproduce
npm install && npm run build && ./repro.sh runtime- The route is a plain
/[slug]page with no runtime data,generateStaticParamsreturnsbuilt,cacheComponents: true. The build shows◐ /[slug]and○ /built. next.config.jssetscacheMaxMemorySize: 0so every read goes to the file-system cache. With the default 50 MB LRU the same happens as soon as the entry is evicted from memory.
- The route is a plain
repro.shstartsnext start, requests/runtime(not prerendered) three times and lists the files the cache wrote:
/runtime request 1 -> status=200 x-nextjs-cache=MISS
/runtime request 2 -> status=200 x-nextjs-cache=MISS
/runtime request 3 -> status=200 x-nextjs-cache=MISS
/built request -> status=200 x-nextjs-cache=HIT
files written for /runtime:
runtime.html
runtime.meta
runtime.segments
node apply-fix.js && ./repro.sh runtime2applies the one-line change proposed below to the installednextand repeats:
/runtime2 request 1 -> status=200 x-nextjs-cache=MISS
/runtime2 request 2 -> status=200 x-nextjs-cache=HIT
/runtime2 request 3 -> status=200 x-nextjs-cache=HIT
files written for /runtime2:
runtime2.html
runtime2.meta
runtime2.rsc
runtime2.segments
Current vs. Expected behavior
Current: a fully static page (no postponed state) rendered at runtime on a PPR-enabled route is persisted by FileSystemCache.set() as .html, .meta and .segments/ but without .rsc. FileSystemCache.get() requires .rsc for every APP_PAGE entry that has no postponed state, the readFile throws, the handler returns null, and the server treats the page as a cache miss: x-nextjs-cache: MISS and a full blocking render on every request once the entry is no longer in the in-memory LRU (or immediately with cacheMaxMemorySize: 0). Pages prerendered at build time are unaffected because the export step writes their .rsc.
Expected: a runtime-rendered entry is read back from disk like a build-time one (HIT/STALE), and client navigations to it are served from the stored flight data.
Where the three code paths disagree (16.3.4, same in 16.4.0-canary.19):
- write,
packages/next/src/server/lib/incremental-cache/file-system-cache.ts(set):.rscis written only when!ctx.isRoutePPREnabled. - read, same file (
get):.rscis read when!ctx.isFallback && (!ctx.isRoutePPREnabled || meta.postponed == null). - build export,
packages/next/src/export/routes/app-page.ts:shouldWriteRsc = !isRoutePPREnabled || (!postponed && !hasFallbackParams), with the comment "for fully static outputs (no postponed state and no fallback params), we can safely emit the route .rsc to support static navigations".
The read side and the export already follow the "fully static output" rule; the runtime write side still uses the older "PPR off" rule, so runtime-rendered fully static entries are never readable.
Proposed fix (one line in set()):
// before
if (!ctx.fetchCache && !ctx.isFallback && !ctx.isRoutePPREnabled) {
// after
if (!ctx.fetchCache && !ctx.isFallback && (!ctx.isRoutePPREnabled || data.postponed == null)) {
apply-fix.js in the repro applies exactly this change to the installed package (to the bundled copies in dist/compiled/next-server/*.runtime.prod.js, which is what the production server executes) and the entry becomes readable, as shown above.
Impact on self-hosted next start with cacheComponents: every page on a dynamic segment that is not enumerated at build time, or that is generated on a fresh pod, is effectively memory-only. With hundreds of such pages the LRU is exhausted in minutes and each first visit pays a blocking render. Related but different: #97457 / #97734 deal with the file-system cache returning null after revalidateTag(..., { expire: 0 }).
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0
Available memory (MB): 49152
Available CPU cores: 14
Binaries:
Node: 24.15.0
npm: 11.12.1
Yarn: N/A
pnpm: N/A
Relevant Packages:
next: 16.3.4
eslint-config-next: N/A
react: 19.2.3
react-dom: 19.2.3
typescript: 7.0.2
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Partial Prerendering (PPR), Cache Components, Incremental Static Regeneration (ISR)
Which stage(s) are affected? (Select all that apply)
next start (local), Other (self-hosted Node.js server)
Additional context
The same behaviour was first observed in a production-like app with cacheComponents, where a startup crawler renders every route of every locale/device variant on the pod: the pages listed in the prerender manifest stayed HIT/STALE, while every runtime-rendered dynamic-segment page turned into MISS with a 150–1200 ms render as soon as the 50 MB LRU evicted it. Checked with NEXT_PRIVATE_DEBUG_CACHE=1 and by placing a .rsc file next to such an entry by hand: with the file present the same entry is served from disk.
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
Run the linked reproduction with npm install && npm run build && ./repro.sh runtime, then inspect packages/next/src/server/lib/incremental-cache/file-system-cache.ts and the export logic in packages/next/src/export/routes/app-page.ts. Verify that runtime-rendered fully static PPR entries write .rsc and subsequent requests return cache HIT/STALE results, including client navigation data.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, node.js, typescript
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100