On November 3, 2026, the Next.js team shipped 16.4.5 as a security release — seven fixes, four of them in the caching layer. One of those four, GHSA-mfr8-8qww-v76h (CVE-2026-119790), is the kind of bug worth reading closely even if you patch and move on in five minutes: an app using "use cache" could, under a specific race, return one root param's cached data to a request for a completely different one. Think a page cached per locale serving the wrong language, or a page cached per tenant serving the wrong tenant's content — the advisory calls it a root-param mix-up, and the mechanism is exactly a cache key that didn't include something it should have.
This is written against Next.js 16.4.5 (verified against the framework's own GitHub security advisory and release history as of November 2026, the fix for versions 16.3.0 through 16.4.4). The bug itself is already patched — if you're not pinned to an old version, npm install next@latest closes it. What's worth your time is why it was possible at all, because it's the clearest real-world illustration this series has had yet of what actually goes into a Next.js cache key, and what happens when two caching layers disagree about it.
What you'll learn
By the end of this article you'll be able to:
- Explain exactly what composes a
"use cache"function's cache key, including the part most people don't know about: which root params it actually reads - Explain what composes an
unstable_cache()key, and why it's a fundamentally different (and narrower) idea than"use cache"'s - Reproduce, conceptually, why nesting
"use cache"insideunstable_cache()is unsupported — and why that safety net still had a hole - Audit your own app for the vulnerable pattern and know exactly what to do about it
- Tell whether you should still be reaching for
unstable_cache()at all in new code
Who this is for: you're using the App Router with "use cache" or unstable_cache() for data fetching, and you want more than "upgrade and forget" — you want to understand what a cache key is actually keyed on, because that's the thing that keeps biting people in every caching system, not just this one. If "use cache" and cacheLife() are brand new to you, this series' Cache Components Explained covers the basics first — worth reading before this one, not required.
Contents
- The bug, shown wrong-way-first
- The mental model: two caches, two ideas of identity
- Staged walkthrough: how the leak actually happened
- Edge cases
- Best practices
- FAQ
- Cheat sheet
- Key takeaways
The bug, shown wrong-way-first
Say you run a docs site with a locale segment in the root layout, and a cached function that greets the reader in their language:
// app/[locale]/layout.tsx — locale is a ROOT param: it comes from the root layout's segment import { locale } from "next/root-params" async function getGreeting() { "use cache" const loc = await locale() return GREETINGS[loc] // { en: "Hello", fr: "Bonjour", de: "Hallo" } } Rendered directly, this is exactly right: Next.js tracks that getGreeting reads the locale root param, so locale becomes part of its cache key. en, fr, and de each get their own cached entry. Nothing here is wrong by itself.
The trouble starts when the same data is also needed somewhere "use cache" doesn't reach on its own — a webhook handler, a background job, anywhere outside a render path — and someone reaches for the other caching primitive instead:
// some route handler, written by someone who just wanted "the cached greeting" import { unstable_cache } from "next/cache" const getCachedGreeting = unstable_cache(getGreeting, ["greeting"]) This looks reasonable — "wrap the cached function so I can call it from here too." It's also exactly the pattern the advisory is about. Calling a "use cache" function through unstable_cache() is supposed to be a hard error: Next.js throws "use cache" inside unstable_cache() is unsupported the moment it happens. That error is the actual safety net here — except, during a narrow window right after a server process starts, it didn't always fire.
The mental model: two caches, two ideas of identity
The reason nesting these two is a problem at all — not just an awkward API combination, but a security bug — is that "use cache" and unstable_cache() compute their cache keys from different inputs, and only one of them knows root params exist.
A "use cache" function's key is built from:
- A build ID, unique per deploy — change the build, invalidate everything.
- A function ID, a hash of the function's location and signature in your code.
- Its serializable arguments, if any.
- Any closed-over variables it reads from an outer scope — these are captured automatically and folded into the key, the same as an explicit argument would be.
- Only the root params it actually reads. Root params are the dynamic segments of the root layout (your
localeinapp/[locale]/layout.tsx, say). They're read through imported getter functions fromnext/root-params, and because those getters are visible, statically-known imports, Next.js can tell exactly which ones a given cached function touches — and keys on only those. A function that never callslocale()doesn't getlocalein its key at all; one that does, does.
An unstable_cache() call's key is built from:
- Its function's arguments, plus a stringified version of the function itself.
- An optional
keyPartsarray you can pass to add more identifiers by hand. - Nothing else. It has no concept of root params. It was never built to — it predates the root-params API entirely, and it has no mechanism for discovering what a wrapped function reads from its environment.
Put those side by side and the shape of the bug is obvious: a "use cache" function that depends on locale is only correctly identified when something tracks locale as part of its identity. unstable_cache() can't do that tracking — it only knows the arguments you handed it and the function's own source. Wrap a locale-dependent "use cache" function in unstable_cache(), and you've created a cached value whose real identity (which locale it's for) isn't part of the key that's supposed to identify it.
Normally this mismatch never gets a chance to matter, because the nesting itself is rejected outright. The bug is specifically about what happened in the narrow window where that rejection didn't happen yet.
Staged walkthrough: how the leak actually happened
Step 1 — steady state, patched or unpatched, no race involved. A request renders the docs page directly. getGreeting() runs as a normal "use cache" call, keyed correctly on locale. Each language gets its own entry. Nothing unusual.
Step 2 — the cold-start race. Right after a server process starts, Next.js deduplicates concurrent calls to the same "use cache" function: if two requests ask for the same thing at nearly the same instant, the second one joins the first one's in-flight computation instead of starting a redundant one. That deduplication is normally harmless — it's a performance optimization. The bug is that an unstable_cache() call, arriving at that exact moment, could join the in-flight fill of a concurrent direct call to the underlying "use cache" function — receiving its result before the "nesting is unsupported" check had a chance to reject it.
Step 3 — the wrong key gets written. The unstable_cache() call now has a result — say, the en greeting, because that's what the concurrent direct render happened to be computing. It stores that result under its own key: function arguments plus stringified function, with no locale in it anywhere, because unstable_cache() never knew locale was involved.
Step 4 — the leak. Every subsequent call to that unstable_cache()-wrapped function — regardless of which locale it's actually being requested for — reads the same entry back, until it's revalidated or evicted. A request meant for fr gets en's greeting. The advisory's own language is careful here: it describes "cached data for one root param value returned to requests for other values," not a single named scenario, because the shape of what leaks depends entirely on what your "use cache" function reads — a locale, a tenant ID, anything scoped at the root layout.
Step 5 — the fix. 16.4.5 closes the race itself, so unstable_cache() can no longer join an in-flight "use cache" fill and sidestep the rejection. The officially stated workaround, if you can't upgrade immediately, is the one you should be doing anyway: never nest "use cache" inside unstable_cache(). Outside the race window, that nesting already throws — so simply not writing the pattern removes the exposure entirely, upgrade or not.
Edge cases
- This needs a process restart to matter. The race is specifically about the first fill after a cold start — not steady-state traffic. Platforms that restart instances frequently (aggressive autoscaling, frequent redeploys, serverless cold starts under load) have a larger exposure window than a long-running server that rarely restarts.
- "It throws normally, so I'm safe" was the trap. Teams who'd actually tested the nested pattern in dev likely saw the expected error and concluded it was blocked. The bug lived specifically in the gap between "this is rejected in the common case" and "this is rejected in every case."
- It doesn't require the function to look dangerous.
getGreetingabove reads no secrets and no per-user data — it's a root param. The lesson generalizes past locale to anything scoped at a root layout: a tenant slug, a region, an environment name. If a"use cache"function reads it throughnext/root-params, it's part of that function's real identity, whether or not the code wrapping it knows that. -
keyPartswould not have fixed this. You might reach forunstable_cache'skeyPartsarray to manually add disambiguating identifiers. That helps for its own arguments, but it doesn't change the underlying problem: nesting a"use cache"function insideunstable_cache()is unsupported regardless of how carefully you key the outer call.
Best practices
- Upgrade now.
next@16.4.5closes the race outright. This is the one unambiguous action item in this whole article. - Grep your codebase for
unstable_cache(wrapping a function that also carries"use cache". That pattern is the exposure, full stop — remove the nesting even after upgrading, since it was never a supported combination. - Treat
unstable_cache()as legacy going forward. The current docs open its reference page with a note that"use cache"replaces it for most use cases in Next.js 16 and point you at Cache Components —unstable_cacheremains supported mainly for non-fetchdata that must persist across deployments. New code shouldn't reach for it. - When you do need caching outside a render path — a webhook, a cron job, a route handler — call your data-fetching function directly and cache that, rather than wrapping an existing
"use cache"function meant for render-time use. - Know what's actually in your cache key before you ship a cached function. This bug is a specific instance of a general rule: a cache key that omits something your function's output depends on is a correctness bug waiting for a day with the wrong timing — this time, a cold start; next time, maybe something else entirely.
FAQ
Do I need to do anything beyond upgrading?
Upgrading to 16.4.5 closes the race, which is the actual vulnerability. (Root params, and therefore this exact bug, don't exist before Next.js 16.3 — there's no 15.x line to patch.) On top of that, removing any unstable_cache( wrapper around a "use cache" function is good hygiene regardless of version, since that nesting was never a supported pattern.
Does this affect apps that don't use root params at all?
The specific CVE is about root-param identity going missing from a cache key. If your "use cache" functions never read a root param (no next/root-params imports), this exact mechanism doesn't apply to your cache keys — but the broader nesting pattern was still unsupported and still worth removing.
Is unstable_cache() being removed?
Not in this release, and the advisory doesn't say that. But the current docs open the unstable_cache reference page with a note that "use cache" replaces it for most use cases in Next.js 16, pointing you at Cache Components instead. Write new code against "use cache".
Does the Pages Router hit this?
No — root params, "use cache", and this specific race are all App Router concepts. The Pages Router has no root-param mechanism for this bug to involve.
How severe is this, really?
The advisory rates it Moderate, CVSS v4 base score 6.3 — network-exploitable, low complexity, but with "attack requirements present" (the cold-start timing window) and only a low confidentiality impact scored at the framework level. Whether it's more serious for your app depends entirely on what your root-param-scoped "use cache" functions return.
What exactly triggers the race?
An unstable_cache() call arriving while the wrapped "use cache" function's very first fill after a process start is still in flight from a concurrent direct call. It's a timing condition tied to process startup, not something a request can reliably force on demand.
Cheat sheet
Next.js cache key composition — what's actually in each one: "use cache" function key: buildId + functionId (hash of location + signature) + serializable arguments + closed-over variables (captured automatically) + ONLY the root params this function actually reads (via next/root-params) unstable_cache() call key: function arguments + stringified function + optional keyParts[] you pass explicitly + NOTHING about root params — it has no mechanism to track them The vulnerable pattern (don't write this, patched or not): async function getData() { "use cache" const val = await someRootParam() // part of THIS function's real identity return ... } const wrapped = unstable_cache(getData, [...]) // unstable_cache can't see that identity GHSA-mfr8-8qww-v76h / CVE-2026-119790 Affected: next >= 16.3.0, <= 16.4.4 Patched: 16.4.5 Note: root params don't exist before 16.3, so the 15.x line was never affected Severity: Moderate, CVSS v4 6.3 Root cause: cold-start race let unstable_cache() join an in-flight "use cache" fill before the "unsupported nesting" check could reject it Workaround (any version): never nest "use cache" inside unstable_cache() Migration direction: unstable_cache() → "use cache" (the docs recommend "use cache" / Cache Components for most use cases as of Next.js 16) 🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
Key takeaways
-
"use cache"andunstable_cache()build their cache keys from genuinely different inputs — one tracks root params automatically, the other has no concept of them at all. - Nesting
"use cache"insideunstable_cache()is unsupported and normally throws — but a narrow cold-start race in versions 16.3.0–16.4.4 let anunstable_cache()call slip past that check and inherit a result keyed without the root param that actually identified it. - The fix is upgrading to 16.4.5; the durable habit is never writing that nesting at all, and treating
unstable_cache()as the legacy API the docs already say it is. - The general lesson outlives this specific CVE: a cache key that's missing something your function's output depends on is a correctness bug first, and sometimes a security bug second — know exactly what's in yours.
A security advisory that gets filed, patched, and forgotten in a five-minute dependency bump is a missed chance to actually understand your own caching layer. This one is worth the extra ten minutes: now you know, precisely, what goes into a Next.js cache key — which is the thing that was actually missing here, not a missing patch.
🧠 Test yourself
Think it clicked? Take the 8-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
📚 Read next
- Next.js Cache Components Explained (with Cheat Sheet)
- Next.js 16 updateTag vs revalidateTag: What Changed
- Next.js 16.4 ensureStatic: Locking Routes Fully Static
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___