Skip to content

Support accessing root params in "use cache" functions#91191

Merged
unstubbable merged 3 commits intocanaryfrom
hl/root-params-in-use-cache
Mar 17, 2026
Merged

Support accessing root params in "use cache" functions#91191
unstubbable merged 3 commits intocanaryfrom
hl/root-params-in-use-cache

Conversation

@unstubbable
Copy link
Contributor

@unstubbable unstubbable commented Mar 11, 2026

Root params (e.g. import { lang } from 'next/root-params') can now be read inside "use cache" functions. The read root param values are automatically included in the cache key so that different root param combinations produce separate cache entries.

Since which root params a cache function reads is only known after execution, the cache key is reconciled post-generation. When root params are read, a two-key scheme is used: the full entry is stored under a specific key (coarse key + root param suffix), and a lightweight redirect entry is stored under the coarse key. The redirect entry's tags encode the root param names using the pattern _N_RP_<rootParamName> (e.g. _N_RP_lang), following the convention of existing internal tags like _N_T_<pathname> (e.g. _N_T_/dashboard) for implicit route tags. This allows a cold server to resolve the specific key on the first request after restart. An in-memory map (knownRootParamsByFunctionId) provides a fast path for subsequent invocations. When no root params are read, the full entry is stored directly under the coarse key with no redirect involved.

The in-memory map grows monotonically — if a function conditionally reads different root params across invocations, the set accumulates all observed param names. The redirect entry's tags are built from this combined set, ensuring cold servers always resolve the most complete specific key.

The two-key scheme only applies to the cache handler. The Resume Data Cache (RDC) always uses the coarse key because each page gets its own isolated RDC instance, so root params are fixed within a single RDC and no disambiguation is needed. When an RDC entry is found during resume, it seeds knownRootParamsByFunctionId so that subsequent cache handler lookups can use the specific key directly.

Reading root params inside unstable_cache still throws. Reading root params inside "use cache" nested within unstable_cache throws with a specific error message explaining the limitation.

Alternatives considered: extending the CacheEntry interface (would be a breaking change for custom cache handlers), encoding root param metadata in the stream via a wrapper object, or sentinel byte, or Flightception (runtime overhead of stream manipulation on every cache read), and deferring cacheHandler.set until after generation (breaks the cache handler's pending-set deduplication for concurrent requests).

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 11, 2026

Tests Passed

@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 3 times, most recently from 4a1b101 to b93df52 Compare March 11, 2026 12:12
@unstubbable unstubbable force-pushed the hl/avoid-undefined-outer-work-unit-store branch from 900705d to 3b3d69a Compare March 11, 2026 12:12
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Mar 11, 2026

Stats from current PR

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 483 MB 483 MB 🔴 +97.3 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 455ms █▁▁▁▁
Cold (Ready in log) 440ms 437ms █▁▁▁▂
Cold (First Request) 1.112s 1.106s █▂▂▁▃
Warm (Listen) 456ms 456ms █▁▁▁▁
Warm (Ready in log) 443ms 445ms █▁▁▁▁
Warm (First Request) 343ms 345ms █▁▁▁▁
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁█▁▁▃
Cold (Ready in log) 437ms 438ms ▃█▃▁▃
Cold (First Request) 1.962s 1.969s ▂█▂▁▂
Warm (Listen) 456ms 456ms ▁█▁▁▁
Warm (Ready in log) 437ms 438ms ▃█▃▁▃
Warm (First Request) 1.977s 1.959s ▂█▂▁▂

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.713s 3.800s █▃▃▂▁
Cached Build 3.820s 3.784s █▃▂▂▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.347s 14.352s ▁█▁▁▁
Cached Build 14.444s 14.446s ▁█▁▁▂
node_modules Size 483 MB 483 MB 🔴 +97.3 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 256 B 253 B 🟢 3 B (-1%)
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 ⚠️ +682 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 ✅ -291 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 615 B 614 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.8 kB 43.8 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.5 kB 45.4 kB ✅ -92 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 🟢 5.23 kB (0%)
index.pack gzip 110 kB 108 kB 🟢 1.26 kB (-1%)
index.pack.old gzip 108 kB 109 kB 🔴 +1.18 kB (+1%)
Total 4.48 MB 4.48 MB ✅ -5.31 kB

🔄 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 332 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 ⚠️ +477 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/daa3b2be221eab9534b98970d026aa7438fbcc2b/next

@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 2 times, most recently from e993281 to 4476ebe Compare March 11, 2026 13:00
@unstubbable unstubbable marked this pull request as ready for review March 11, 2026 14:23
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 2 times, most recently from 0e62923 to 88960d2 Compare March 13, 2026 21:42
@unstubbable unstubbable force-pushed the hl/avoid-undefined-outer-work-unit-store branch from 3b3d69a to 0a35f46 Compare March 13, 2026 21:42
@unstubbable unstubbable requested review from gnoff and lubieowoce March 13, 2026 21:43
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from 88960d2 to cd95606 Compare March 13, 2026 21:48
@unstubbable unstubbable force-pushed the hl/avoid-undefined-outer-work-unit-store branch from 0a35f46 to 5bc448f Compare March 13, 2026 21:48
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from cd95606 to a0d54b5 Compare March 13, 2026 21:57
@unstubbable unstubbable force-pushed the hl/avoid-undefined-outer-work-unit-store branch from 5bc448f to f36358a Compare March 16, 2026 14:47
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 2 times, most recently from f459f4e to af0bc89 Compare March 16, 2026 14:49
@unstubbable unstubbable force-pushed the hl/avoid-undefined-outer-work-unit-store branch from f36358a to 381a437 Compare March 16, 2026 14:49
Comment on lines +207 to +210
// The RDC is per-page and root params are fixed within a page, so we always
// use the coarse key (without root param suffix). Unlike the cache handler,
// the RDC doesn't need root-param-specific keys for isolation.
prerenderResumeDataCache.cache.set(serializedCacheKey, rdcResult)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess this is fine, just feels like something that might bite us later...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It's the same premise that allows us to put 'use cache: private' into the RDC without including the read cookies in the cache key, isn't it?

Copy link
Member

@lubieowoce lubieowoce Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the 'use cache: private' entries aren't persisted anywhere, so it's less observable.

this here will be persisted along the prerender. i think previously the keys in the serialized RDC would always be equivalent to the keys the cache handler has, but now that'll change.

so If we did something like, idk, seeding the in-memory cache using entries from RDC on first request, that could become a problem, because we might accidentally seed the coarse entry (which should contain a redirect) with data for a fine entry UNLESS you remember this special case
(that specific use-case likely wouldn't make sense, bc we can just read from RDC instead, but i can imagine this shape of problem coming up in the future in some way)

i don't think this is a blocker, it'll work fine, but i guess i'm just wondering if we need to introduce this fork in behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It avoids having to add redirect entries to the RDC as well, which avoids runtime overhead and keeps the ISR payload size smaller.

Comment on lines +228 to +229
// read no root params, a previous invocation may have — and we need to
// match the lookup key from that union.
Copy link
Member

@lubieowoce lubieowoce Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and we need to match the lookup key from that union.

this thing about matching the lookup key is a bit confusing to me, because if the current invocation read more root params than a past one, we also wouldn't match the old key, so what happens then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rephrase that question, maybe spelling out a concrete scenario that you have in mind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment talks about a scenario where past invocations read more params than the current invocation. It doesn't say what should happen if it's the opposite, and the current invocation read more params than past ones. It seems like in both cases we'd have a potential mismatch that needs handling, so why does this place only call out the former, but not the latter?

Copy link
Contributor Author

@unstubbable unstubbable Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought using readRootParamNames (the case where we might have read more than previously) was kinda obvious, so I only called out the less obvious case where we also need to include the previously read root params.

@unstubbable unstubbable changed the base branch from hl/avoid-undefined-outer-work-unit-store to graphite-base/91191 March 16, 2026 15:40
@unstubbable unstubbable force-pushed the graphite-base/91191 branch from 381a437 to 9bec0cc Compare March 16, 2026 15:40
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from af0bc89 to f50f505 Compare March 16, 2026 15:40
@graphite-app graphite-app bot changed the base branch from graphite-base/91191 to canary March 16, 2026 15:41
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch 6 times, most recently from 8a7b9c8 to daa3b2b Compare March 16, 2026 21:04
@unstubbable unstubbable requested a review from lubieowoce March 16, 2026 21:38
Root params (e.g. `import { lang } from 'next/root-params'`) can now be
read inside `"use cache"` functions. The read root param values are
automatically included in the cache key so that different root param
combinations produce separate cache entries.

Since which root params a cache function reads is only known after
execution, the cache key is reconciled post-generation. When root params
are read, a two-key scheme is used: the full entry is stored under a
specific key (coarse key + root param suffix), and a lightweight
redirect entry is stored under the coarse key. The redirect entry's tags
encode the root param names using the pattern `_N_RP_<rootParamName>`
(e.g. `_N_RP_lang`), following the convention of existing internal tags
like `_N_T_<pathname>` (e.g. `_N_T_/dashboard`) for implicit route tags.
This allows a cold server to resolve the specific key on the first
request after restart. An in-memory map (`knownRootParamsByFunctionId`)
provides a fast path for subsequent invocations. When no root params are
read, the full entry is stored directly under the coarse key with no
redirect involved.

The in-memory map grows monotonically — if a function conditionally
reads different root params across invocations, the set accumulates all
observed param names. The redirect entry's tags are built from this
combined set, ensuring cold servers always resolve the most complete
specific key.

The two-key scheme only applies to the cache handler. The Resume Data
Cache (RDC) always uses the coarse key because each page gets its own
isolated RDC instance, so root params are fixed within a single RDC and
no disambiguation is needed. When an RDC entry is found during resume,
it seeds `knownRootParamsByFunctionId` so that subsequent cache handler
lookups can use the specific key directly.

Reading root params inside `unstable_cache` still throws. Reading root
params inside `"use cache"` nested within `unstable_cache` throws with a
specific error message explaining the limitation.

Alternatives considered: extending the `CacheEntry` interface (would be
a breaking change for custom cache handlers), encoding root param
metadata in the stream via a wrapper object, or sentinel byte, or
Flightception (runtime overhead of stream manipulation on every cache
read), and deferring `cacheHandler.set` until after generation (breaks
the cache handler's pending-set deduplication for concurrent requests).
@unstubbable unstubbable force-pushed the hl/root-params-in-use-cache branch from daa3b2b to 6282a5a Compare March 16, 2026 23:13
@unstubbable unstubbable merged commit 8283b12 into canary Mar 17, 2026
159 checks passed
@unstubbable unstubbable deleted the hl/root-params-in-use-cache branch March 17, 2026 13:04
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.

3 participants