Skip to content

Fix inconsistent cache life/tags propagation for cache handler hits#91454

Open
unstubbable wants to merge 1 commit intohl/root-params-in-use-cachefrom
hl/fix-cache-handler-hit-propagation
Open

Fix inconsistent cache life/tags propagation for cache handler hits#91454
unstubbable wants to merge 1 commit intohl/root-params-in-use-cachefrom
hl/fix-cache-handler-hit-propagation

Conversation

@unstubbable
Copy link
Contributor

@unstubbable unstubbable commented Mar 16, 2026

When a "use cache" entry is newly generated during a prerender (prerender or prerender-runtime), collectResult defers propagation of cache life and tags to the outer context. This is because the entry might later be omitted from the final prerender due to short expire or stale times, and omitted entries should not affect the prerender's cache life.

However, when a cache handler returns a hit for an existing entry, propagateCacheEntryMetadata was called unconditionally, without the same deferral logic. This meant that short-lived cache entries retrieved from the cache handler could propagate their cache life to the prerender store, even though they would later be omitted from the final render.

This inconsistency is currently not observable because runtime prefetches use a prospective and final two-store architecture (see prospectiveRuntimeServerPrerender and finalRuntimeServerPrerender in app-render.tsx). The cache handler hit propagation corrupts the prospective store, but the response is produced from the final store, which reads from the resume data cache with correct stale and expire checks. Static prerenders have a similar two-phase architecture that masks the issue. Because of this, there is no test case that can observe the incorrect behavior, but the fix avoids confusion and prevents the inconsistency from becoming a real bug if the architecture changes.

This change extracts a maybePropagateCacheEntryMetadata function that encapsulates the conditional propagation logic and is now called from both the generation path (inside collectResult) and the cache handler hit path. The resume data cache read path continues to call propagateCacheEntryMetadata unconditionally, since it runs in the final render phase after short-lived entries have already been filtered out.

Copy link
Contributor Author

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 16, 2026

Failing test suites

Commit: bd20bd2 | About building and testing Next.js

pnpm test-start-turbo test/production/graceful-shutdown/index.test.ts (turbopack) (job)

  • Graceful Shutdown > production (standalone mode) > should not accept new requests during shutdown cleanup > should stop accepting new requests when shutting down (DD)
Expand output

● Graceful Shutdown › production (standalone mode) › should not accept new requests during shutdown cleanup › should stop accepting new requests when shutting down

expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
-   143,
    null,
+   "SIGTERM",
  ]

  227 |
  228 |         // App finally shuts down with signal-based exit code (128 + 15 for SIGTERM)
> 229 |         expect(await appKilledPromise).toEqual([143, null])
      |                                        ^
  230 |         expect(app.exitCode).toBe(143)
  231 |       })
  232 |     })

  at Object.toEqual (production/graceful-shutdown/index.test.ts:229:40)

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 16, 2026

Stats from current PR

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 483 MB 483 MB 🔴 +104 kB (+0%) █████
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms █▁▁▁▁
Cold (Ready in log) 438ms 437ms █▁▁▁▂
Cold (First Request) 1.152s 1.143s █▂▂▁▃
Warm (Listen) 457ms 456ms █▁▁▁▁
Warm (Ready in log) 442ms 443ms █▁▁▁▁
Warm (First Request) 342ms 346ms █▁▁▁▁
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 507ms 507ms ▁█▁▁▃
Cold (Ready in log) 471ms 472ms ▃█▃▁▃
Cold (First Request) 2.075s 2.065s ▂█▂▁▂
Warm (Listen) 507ms 508ms ▁█▁▁▁
Warm (Ready in log) 472ms 473ms ▃█▃▁▃
Warm (First Request) 2.088s 2.096s ▂█▂▁▂

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.761s 3.793s █▃▃▂▁
Cached Build 3.791s 3.784s █▃▂▂▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 15.512s 15.522s ▁█▁▁▁
Cached Build 15.650s 15.700s ▁█▁▁▂
node_modules Size 483 MB 483 MB 🔴 +104 kB (+0%) █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
0_lpxhkyog9-9.js gzip 162 B N/A -
0.-r71sd1j_zb.js gzip 7.61 kB N/A -
0~dh.pe76tp4s.js gzip 154 B N/A -
0~lwfcrlb4v_9.css gzip 115 B 115 B
00h0nz7r436~l.js gzip 13.3 kB N/A -
010veokj5t9nf.js gzip 169 B N/A -
02ku7edzc_wf7.js gzip 450 B N/A -
03~yq9q893hmn.js gzip 39.4 kB 39.4 kB
034~ea1.fumud.js gzip 48.5 kB N/A -
06ifmqyzqzamf.js gzip 157 B N/A -
092lcb3fqrrf9.js gzip 8.52 kB N/A -
0aj~xs1l1g8tg.js gzip 8.53 kB N/A -
0eg78sqvyqa0_.js gzip 13.7 kB N/A -
0ehfdrc23-o0h.js gzip 154 B N/A -
0h35gmp9u328z.js gzip 8.54 kB N/A -
0h6fkavebp.iz.js gzip 8.47 kB N/A -
0i1_a9rimqo25.js gzip 70.8 kB N/A -
0ino_yf1k3h6k.js gzip 10.4 kB N/A -
0k.sixc712dq1.js gzip 10.1 kB N/A -
0kkm7tesfinr6.js gzip 12.9 kB N/A -
0moy~uao4dl.m.js gzip 9.19 kB N/A -
0n~bmvrddbhmi.js gzip 154 B N/A -
0q50rtpusjy90.js gzip 2.28 kB N/A -
0smgy2grrrlka.js gzip 8.58 kB N/A -
0sv1h.1~4785f.js gzip 156 B N/A -
0syjr-z700d-h.js gzip 163 B N/A -
0t1dzhdfh0txh.js gzip 215 B 215 B
0w_gs_7ptie.q.js gzip 156 B N/A -
0y0~bkd6ge6gu.js gzip 65.7 kB N/A -
0zid7o0-vupvp.js gzip 225 B N/A -
109-8lzwgo3p0.js gzip 158 B N/A -
11yo3xfd6b147.js gzip 12.9 kB N/A -
13.84hqxl_1p7.js gzip 9.76 kB N/A -
14ob4-.af1o4f.js gzip 154 B N/A -
1554wr-t7p6z-.js gzip 8.55 kB N/A -
15tjst79~qy3_.js gzip 1.46 kB N/A -
15z_v00ne4ud0.js gzip 8.47 kB N/A -
16yzjq-v.qe0c.js gzip 156 B N/A -
17d_m3p4j9w6r.js gzip 5.62 kB N/A -
17yu~3yiu7d2m.js gzip 8.52 kB N/A -
1808zda6e6e_u.js gzip 160 B N/A -
turbopack-0d..8tpw.js gzip 4.17 kB N/A -
turbopack-0f..9a0w.js gzip 4.16 kB N/A -
turbopack-0i..zgu_.js gzip 4.16 kB N/A -
turbopack-0j..a72m.js gzip 4.16 kB N/A -
turbopack-0k..0-f4.js gzip 4.16 kB N/A -
turbopack-0n..f5~n.js gzip 4.16 kB N/A -
turbopack-0o..0-9y.js gzip 4.14 kB N/A -
turbopack-0q..m0x3.js gzip 4.16 kB N/A -
turbopack-0r..qsv_.js gzip 4.16 kB N/A -
turbopack-0y..4sn_.js gzip 4.16 kB N/A -
turbopack-14...pe_.js gzip 4.16 kB N/A -
turbopack-15..dibb.js gzip 4.16 kB N/A -
turbopack-16..ranj.js gzip 4.16 kB N/A -
turbopack-17..76x..js gzip 4.16 kB N/A -
0_a4ve.qo0cly.js gzip N/A 156 B -
00-x1a0mnl.17.js gzip N/A 158 B -
03t__~.5lvgeu.js gzip N/A 5.62 kB -
044nffvczz1qm.js gzip N/A 70.8 kB -
04d6ll75jqx3r.js gzip N/A 9.19 kB -
0583exyh-yhc7.js gzip N/A 9.76 kB -
05b07nana~4_..js gzip N/A 10.1 kB -
06cc8lt1-ai~5.js gzip N/A 157 B -
072lv63r8dcz~.js gzip N/A 8.58 kB -
07k6dcww5s4pu.js gzip N/A 13.7 kB -
0ar1~bwpezfgw.js gzip N/A 13.3 kB -
0c99mq1ez2bke.js gzip N/A 450 B -
0cq-cmde_ws6u.js gzip N/A 8.47 kB -
0e7xd7-t.gkzq.js gzip N/A 156 B -
0fwf102w10o9~.js gzip N/A 8.52 kB -
0gtmn.q_j1v5r.js gzip N/A 10.4 kB -
0i_z76m40njyo.js gzip N/A 162 B -
0l~38duq8a8dv.js gzip N/A 65.7 kB -
0lf9izkd8uzut.js gzip N/A 48.4 kB -
0lx8zzip869ie.js gzip N/A 161 B -
0nclq9z6yzzm5.js gzip N/A 1.46 kB -
0nzumcogektg7.js gzip N/A 8.55 kB -
0o2ue-3nbm5kc.js gzip N/A 154 B -
0on3338gqfhmf.js gzip N/A 158 B -
0pnpi9xx-xqtf.js gzip N/A 169 B -
0q8a65hxfloxk.js gzip N/A 158 B -
0s.c-cn5eebrx.js gzip N/A 8.47 kB -
0tna7lg6q4zne.js gzip N/A 12.9 kB -
0votdfxr5fb5u.js gzip N/A 2.28 kB -
0xkuhv202qqhu.js gzip N/A 7.6 kB -
0ykl9bs_qj.5..js gzip N/A 8.52 kB -
0zfen0tnxp4gh.js gzip N/A 8.55 kB -
10wkq1h9jzkg..js gzip N/A 225 B -
11zz80.wdrfoo.js gzip N/A 158 B -
1447c.do.01sf.js gzip N/A 157 B -
149ndfh8zfcaz.js gzip N/A 8.53 kB -
14u1b~e_aixr3.js gzip N/A 157 B -
15gkb_10omqgr.js gzip N/A 13 kB -
turbopack-0_..x0yh.js gzip N/A 4.16 kB -
turbopack-02..8md_.js gzip N/A 4.16 kB -
turbopack-04..m54t.js gzip N/A 4.16 kB -
turbopack-07..81e5.js gzip N/A 4.16 kB -
turbopack-08..6.dx.js gzip N/A 4.16 kB -
turbopack-0c..ylkb.js gzip N/A 4.16 kB -
turbopack-0d..-iw2.js gzip N/A 4.16 kB -
turbopack-0n..rv9o.js gzip N/A 4.16 kB -
turbopack-0o..6jfy.js gzip N/A 4.16 kB -
turbopack-0o..lgl~.js gzip N/A 4.16 kB -
turbopack-0q..0hr3.js gzip N/A 4.16 kB -
turbopack-0s..9o4o.js gzip N/A 4.14 kB -
turbopack-14..imhv.js gzip N/A 4.16 kB -
turbopack-17..mkxz.js gzip N/A 4.18 kB -
Total 463 kB 463 kB ⚠️ +36 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 712 B 711 B
Total 712 B 711 B ✅ -1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 433 B 430 B
Total 433 B 430 B ✅ -3 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 60.3 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.7 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.2 kB 39.2 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.7 kB -
9544-HASH.js gzip N/A 60.9 kB -
Total 234 kB 235 kB ⚠️ +684 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.98 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 269 kB 268 kB
Total 393 kB 393 kB ✅ -367 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 615 B 615 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.9 kB 44 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.5 kB 45.6 kB ⚠️ +20 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.26 MB 4.26 MB
index.pack gzip 108 kB 109 kB
index.pack.old gzip 108 kB 109 kB
Total 4.48 MB 4.48 MB ✅ -770 B

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 333 kB 333 kB
app-page-exp..prod.js gzip 181 kB 181 kB
app-page-tur...dev.js gzip 332 kB 333 kB
app-page-tur..prod.js gzip 181 kB 181 kB
app-page-tur...dev.js gzip 329 kB 329 kB
app-page-tur..prod.js gzip 179 kB 179 kB
app-page.run...dev.js gzip 329 kB 329 kB
app-page.run..prod.js gzip 179 kB 179 kB
app-route-ex...dev.js gzip 76 kB 76 kB
app-route-ex..prod.js gzip 51.7 kB 51.7 kB
app-route-tu...dev.js gzip 76 kB 76 kB
app-route-tu..prod.js gzip 51.7 kB 51.7 kB
app-route-tu...dev.js gzip 75.6 kB 75.6 kB
app-route-tu..prod.js gzip 51.5 kB 51.5 kB
app-route.ru...dev.js gzip 75.6 kB 75.6 kB
app-route.ru..prod.js gzip 51.5 kB 51.5 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.3 kB 43.4 kB
pages-api-tu..prod.js gzip 33 kB 33 kB
pages-api.ru...dev.js gzip 43.3 kB 43.3 kB
pages-api.ru..prod.js gzip 33 kB 33 kB
pages-turbo....dev.js gzip 52.7 kB 52.7 kB
pages-turbo...prod.js gzip 38.6 kB 38.6 kB
pages.runtim...dev.js gzip 52.7 kB 52.7 kB
pages.runtim..prod.js gzip 38.6 kB 38.6 kB
server.runti..prod.js gzip 62.4 kB 62.4 kB
Total 2.95 MB 2.95 MB ⚠️ +484 B
📝 Changed Files (16 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • app-route-ex..ntime.dev.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..ntime.dev.js
  • app-route.runtime.dev.js
  • pages-api-tu..ntime.dev.js
  • pages-api.runtime.dev.js
  • pages-turbo...ntime.dev.js
  • pages.runtime.dev.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

app-route-ex..ntime.dev.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route.runtime.dev.js

Diff too large to display

pages-api-tu..ntime.dev.js

Diff too large to display

pages-api.runtime.dev.js

Diff too large to display

pages-turbo...ntime.dev.js

Diff too large to display

pages.runtime.dev.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/47d46def2675b981cbfc3af5e1a9b1af36a55724/next

@unstubbable unstubbable marked this pull request as ready for review March 16, 2026 13:45
@unstubbable unstubbable requested a review from lubieowoce March 16, 2026 13:45
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from a0d54b5 to f459f4e Compare March 16, 2026 14:47
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch from 7c5bb1c to 6cdbd8c Compare March 16, 2026 14:47
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from f459f4e to af0bc89 Compare March 16, 2026 14:49
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch from 6cdbd8c to a861e7c Compare March 16, 2026 14:49
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 2 times, most recently from f50f505 to 1034a3f Compare March 16, 2026 15:41
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch from a861e7c to f63764d Compare March 16, 2026 15:41
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from 1034a3f to 0bf6951 Compare March 16, 2026 18:16
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch 2 times, most recently from 6b94301 to 1d7a484 Compare March 16, 2026 18:23
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from b560a6e to 68df0ff Compare March 16, 2026 19:55
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch 2 times, most recently from ae318fc to b36c41f Compare March 16, 2026 20:00
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from 68df0ff to b93c42c Compare March 16, 2026 20:00
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch from b36c41f to 940a612 Compare March 16, 2026 20:32
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 2 times, most recently from 8a7b9c8 to daa3b2b Compare March 16, 2026 21:04
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch from 940a612 to 47d46de Compare March 16, 2026 21:04
When a `"use cache"` entry is newly generated during a prerender
(`prerender` or `prerender-runtime`), `collectResult` defers propagation
of cache life and tags to the outer context. This is because the entry
might later be omitted from the final prerender due to short expire or
stale times, and omitted entries should not affect the prerender's cache
life.

However, when a cache handler returns a hit for an existing entry,
`propagateCacheEntryMetadata` was called unconditionally, without the
same deferral logic. This meant that short-lived cache entries retrieved
from the cache handler could propagate their cache life to the prerender
store, even though they would later be omitted from the final render.

This inconsistency is currently not observable because runtime
prefetches use a prospective and final two-store architecture (see
`prospectiveRuntimeServerPrerender` and `finalRuntimeServerPrerender` in
`app-render.tsx`). The cache handler hit propagation corrupts the
prospective store, but the response is produced from the final store,
which reads from the resume data cache with correct stale and expire
checks. Static prerenders have a similar two-phase architecture that
masks the issue. Because of this, there is no test case that can observe
the incorrect behavior, but the fix avoids confusion and prevents the
inconsistency from becoming a real bug if the architecture changes.

This change extracts a `maybePropagateCacheEntryMetadata` function that
encapsulates the conditional propagation logic and is now called from
both the generation path (inside `collectResult`) and the cache handler
hit path. The resume data cache read path continues to call
`propagateCacheEntryMetadata` unconditionally, since it runs in the
final render phase after short-lived entries have already been filtered
out.
@unstubbable unstubbable force-pushed the hl/fix-cache-handler-hit-propagation branch from 47d46de to bd20bd2 Compare March 16, 2026 23:13
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from daa3b2b to 6282a5a Compare March 16, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants