[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-nextjs-weekly-unstable-cache-root-params-leak":44,"search-suggestions":404,"path-membership-nextjs-weekly-unstable-cache-root-params-leak":450,"related-nextjs-weekly-unstable-cache-root-params-leak":451,"code:tsx:true:6824ge":557,"code:ts:true:1mz4fcw":558,"code:plain:true:3gx97i":559,"comments-01a1b128-0d77-74cd-8d38-6dda8c5a843a":560},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":74,"playground":76,"body":78,"bodyMd":340,"seo":341,"translationGroupId":343,"series":344,"podcastUrl":48,"verticalId":5,"thread":392,"assessments":394,"translations":400,"quiz":402},"01a1b128-0d77-74cd-8d38-6dda8c5a843a","nextjs-weekly-unstable-cache-root-params-leak","Next.js 16.4.5: How unstable_cache Leaked Data Across Root Params",null,"Next.js 16.4.5 patched a real bug: unstable_cache() could return another root param's cached data. Learn how cache keys work, plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-unstable-cache-root-params-leak.png",13,"2026-11-10T14:37:08.009Z",7,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"nextjs","Nextjs",[64,65,68,71],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},"security","Security",{"slug":69,"name":70,"color":48},"caching","Caching",{"slug":72,"name":73,"color":48},"tutorial","Tutorial",{"assessments":75},1,{"slug":46,"title":77},"Next.js unstable_cache cache-key leak — interactive playground",{"blocks":79,"version":75},[80,84,87,92,95,104,107,110,121,124,127,133,136,139,144,147,150,153,156,164,167,173,176,179,182,185,188,191,194,197,200,207,210,218,221,224,227,231,234,238,241,244,247,250,253,256,259,262,267,270,273,276,279,282,285,292,295,298,301,304,307,310,313,316,322,325,328,331,334],{"id":81,"html":82,"type":83},"b1","\u003Cp>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, \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fvercel\u002Fnext.js\u002Fsecurity\u002Fadvisories\u002FGHSA-mfr8-8qww-v76h\">GHSA-mfr8-8qww-v76h\u003C\u002Fa> (CVE-2026-119790), is the kind of bug worth reading closely even if you patch and move on in five minutes: an app using \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> could, under a specific race, return one root param&#39;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&#39;s content — the advisory calls it a root-param mix-up, and the mechanism is exactly a cache key that didn&#39;t include something it should have.\u003C\u002Fp>","paragraph",{"id":85,"html":86,"type":83},"b2","\u003Cp>This is written against \u003Cstrong>Next.js 16.4.5\u003C\u002Fstrong> (verified against the framework&#39;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&#39;re not pinned to an old version, \u003Ccode>npm install next@latest\u003C\u002Fcode> closes it. What&#39;s worth your time is \u003Cem>why\u003C\u002Fem> it was possible at all, because it&#39;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.\u003C\u002Fp>",{"id":88,"html":89,"text":90,"type":91,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":93,"html":94,"type":83},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":96,"type":97,"items":98,"ordered":18},"b5","list",[99,100,101,102,103],"Explain exactly what composes a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function&#39;s cache key, including the part most people don&#39;t know about: which root params it actually reads","Explain what composes an \u003Ccode>unstable_cache()\u003C\u002Fcode> key, and why it&#39;s a fundamentally different (and narrower) idea than \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>&#39;s","Reproduce, conceptually, why nesting \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> inside \u003Ccode>unstable_cache()\u003C\u002Fcode> 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 \u003Ccode>unstable_cache()\u003C\u002Fcode> at all in new code",{"id":105,"html":106,"type":83},"b6","\u003Cp>\u003Cstrong>Who this is for:\u003C\u002Fstrong> you&#39;re using the App Router with \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> or \u003Ccode>unstable_cache()\u003C\u002Fcode> for data fetching, and you want more than &quot;upgrade and forget&quot; — you want to understand what a cache key is actually keyed on, because that&#39;s the thing that keeps biting people in every caching system, not just this one. If \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> and \u003Ccode>cacheLife()\u003C\u002Fcode> are brand new to you, this series&#39; \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob\">Cache Components Explained\u003C\u002Fa> covers the basics first — worth reading before this one, not required.\u003C\u002Fp>",{"id":108,"html":109,"text":109,"type":91,"level":31},"b7","Contents",{"id":111,"type":97,"items":112,"ordered":18},"b8",[113,114,115,116,117,118,119,120],"\u003Ca href=\"#the-bug-shown-wrong-way-first\">The bug, shown wrong-way-first\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-two-caches-two-ideas-of-identity\">The mental model: two caches, two ideas of identity\u003C\u002Fa>","\u003Ca href=\"#staged-walkthrough-how-the-leak-actually-happened\">Staged walkthrough: how the leak actually happened\u003C\u002Fa>","\u003Ca href=\"#edge-cases\">Edge cases\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":122,"html":123,"text":123,"type":91,"level":31},"b9","The bug, shown wrong-way-first",{"id":125,"html":126,"type":83},"b10","\u003Cp>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:\u003C\u002Fp>",{"id":128,"code":129,"type":130,"language":131,"highlight":132},"b11","\u002F\u002F app\u002F[locale]\u002Flayout.tsx — locale is a ROOT param: it comes from the root layout's segment\nimport { locale } from \"next\u002Froot-params\"\n\nasync function getGreeting() {\n  \"use cache\"\n  const loc = await locale()\n  return GREETINGS[loc] \u002F\u002F { en: \"Hello\", fr: \"Bonjour\", de: \"Hallo\" }\n}","code","tsx",[],{"id":134,"html":135,"type":83},"b12","\u003Cp>Rendered directly, this is exactly right: Next.js tracks that \u003Ccode>getGreeting\u003C\u002Fcode> reads the \u003Ccode>locale\u003C\u002Fcode> root param, so \u003Ccode>locale\u003C\u002Fcode> becomes part of its cache key. \u003Ccode>en\u003C\u002Fcode>, \u003Ccode>fr\u003C\u002Fcode>, and \u003Ccode>de\u003C\u002Fcode> each get their own cached entry. Nothing here is wrong by itself.\u003C\u002Fp>",{"id":137,"html":138,"type":83},"b13","\u003Cp>The trouble starts when the same data is also needed somewhere \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> doesn&#39;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:\u003C\u002Fp>",{"id":140,"code":141,"type":130,"language":142,"highlight":143},"b14","\u002F\u002F some route handler, written by someone who just wanted \"the cached greeting\"\nimport { unstable_cache } from \"next\u002Fcache\"\n\nconst getCachedGreeting = unstable_cache(getGreeting, [\"greeting\"])","ts",[],{"id":145,"html":146,"type":83},"b15","\u003Cp>This looks reasonable — &quot;wrap the cached function so I can call it from here too.&quot; It&#39;s also exactly the pattern the advisory is about. Calling a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function through \u003Ccode>unstable_cache()\u003C\u002Fcode> is supposed to be a hard error: Next.js throws \u003Ccode>&quot;use cache&quot; inside unstable_cache() is unsupported\u003C\u002Fcode> 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&#39;t always fire.\u003C\u002Fp>",{"id":148,"html":149,"text":149,"type":91,"level":31},"b16","The mental model: two caches, two ideas of identity",{"id":151,"html":152,"type":83},"b17","\u003Cp>The reason nesting these two is a problem at all — not just an awkward API combination, but a security bug — is that \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> and \u003Ccode>unstable_cache()\u003C\u002Fcode> compute their cache keys from \u003Cstrong>different inputs\u003C\u002Fstrong>, and only one of them knows root params exist.\u003C\u002Fp>",{"id":154,"html":155,"type":83},"b18","\u003Cp>\u003Cstrong>A \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function&#39;s key is built from:\u003C\u002Fstrong>\u003C\u002Fp>",{"id":157,"type":97,"items":158,"ordered":18},"b19",[159,160,161,162,163],"A \u003Cstrong>build ID\u003C\u002Fstrong>, unique per deploy — change the build, invalidate everything.","A \u003Cstrong>function ID\u003C\u002Fstrong>, a hash of the function&#39;s location and signature in your code.","Its \u003Cstrong>serializable arguments\u003C\u002Fstrong>, if any.","Any \u003Cstrong>closed-over variables\u003C\u002Fstrong> it reads from an outer scope — these are captured automatically and folded into the key, the same as an explicit argument would be.","\u003Cstrong>Only the root params it actually reads.\u003C\u002Fstrong> Root params are the dynamic segments of the \u003Cem>root\u003C\u002Fem> layout (your \u003Ccode>locale\u003C\u002Fcode> in \u003Ccode>app\u002F[locale]\u002Flayout.tsx\u003C\u002Fcode>, say). They&#39;re read through imported getter functions from \u003Ccode>next\u002Froot-params\u003C\u002Fcode>, 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 calls \u003Ccode>locale()\u003C\u002Fcode> doesn&#39;t get \u003Ccode>locale\u003C\u002Fcode> in its key at all; one that does, does.",{"id":165,"html":166,"type":83},"b20","\u003Cp>\u003Cstrong>An \u003Ccode>unstable_cache()\u003C\u002Fcode> call&#39;s key is built from:\u003C\u002Fstrong>\u003C\u002Fp>",{"id":168,"type":97,"items":169,"ordered":18},"b21",[170,171,172],"Its \u003Cstrong>function&#39;s arguments\u003C\u002Fstrong>, plus a \u003Cstrong>stringified version of the function itself\u003C\u002Fstrong>.","An optional \u003Cstrong>\u003Ccode>keyParts\u003C\u002Fcode>\u003C\u002Fstrong> array you can pass to add more identifiers by hand.","\u003Cstrong>Nothing else.\u003C\u002Fstrong> 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.",{"id":174,"html":175,"type":83},"b22","\u003Cp>Put those side by side and the shape of the bug is obvious: a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function that depends on \u003Ccode>locale\u003C\u002Fcode> is \u003Cem>only\u003C\u002Fem> correctly identified when something tracks \u003Ccode>locale\u003C\u002Fcode> as part of its identity. \u003Ccode>unstable_cache()\u003C\u002Fcode> can&#39;t do that tracking — it only knows the arguments you handed it and the function&#39;s own source. Wrap a locale-dependent \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function in \u003Ccode>unstable_cache()\u003C\u002Fcode>, and you&#39;ve created a cached value whose real identity (which locale it&#39;s for) isn&#39;t part of the key that&#39;s supposed to identify it.\u003C\u002Fp>",{"id":177,"html":178,"type":83},"b23","\u003Cp>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&#39;t happen yet.\u003C\u002Fp>",{"id":180,"html":181,"text":181,"type":91,"level":31},"b24","Staged walkthrough: how the leak actually happened",{"id":183,"html":184,"type":83},"b25","\u003Cp>\u003Cstrong>Step 1 — steady state, patched or unpatched, no race involved.\u003C\u002Fstrong> A request renders the docs page directly. \u003Ccode>getGreeting()\u003C\u002Fcode> runs as a normal \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> call, keyed correctly on \u003Ccode>locale\u003C\u002Fcode>. Each language gets its own entry. Nothing unusual.\u003C\u002Fp>",{"id":186,"html":187,"type":83},"b26","\u003Cp>\u003Cstrong>Step 2 — the cold-start race.\u003C\u002Fstrong> Right after a server process starts, Next.js deduplicates concurrent calls to the same \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function: if two requests ask for the same thing at nearly the same instant, the second one joins the first one&#39;s in-flight computation instead of starting a redundant one. That deduplication is normally harmless — it&#39;s a performance optimization. The bug is that an \u003Ccode>unstable_cache()\u003C\u002Fcode> call, arriving at that exact moment, could join the in-flight fill of a \u003Cstrong>concurrent direct call to the underlying \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function\u003C\u002Fstrong> — receiving its result \u003Cstrong>before the &quot;nesting is unsupported&quot; check had a chance to reject it\u003C\u002Fstrong>.\u003C\u002Fp>",{"id":189,"html":190,"type":83},"b27","\u003Cp>\u003Cstrong>Step 3 — the wrong key gets written.\u003C\u002Fstrong> The \u003Ccode>unstable_cache()\u003C\u002Fcode> call now has a result — say, the \u003Ccode>en\u003C\u002Fcode> greeting, because that&#39;s what the concurrent direct render happened to be computing. It stores that result under \u003Cem>its own\u003C\u002Fem> key: function arguments plus stringified function, with no \u003Ccode>locale\u003C\u002Fcode> in it anywhere, because \u003Ccode>unstable_cache()\u003C\u002Fcode> never knew \u003Ccode>locale\u003C\u002Fcode> was involved.\u003C\u002Fp>",{"id":192,"html":193,"type":83},"b28","\u003Cp>\u003Cstrong>Step 4 — the leak.\u003C\u002Fstrong> Every subsequent call to that \u003Ccode>unstable_cache()\u003C\u002Fcode>-wrapped function — regardless of which locale it&#39;s actually being requested for — reads the same entry back, until it&#39;s revalidated or evicted. A request meant for \u003Ccode>fr\u003C\u002Fcode> gets \u003Ccode>en\u003C\u002Fcode>&#39;s greeting. The advisory&#39;s own language is careful here: it describes &quot;cached data for one root param value returned to requests for other values,&quot; not a single named scenario, because the shape of what leaks depends entirely on what your \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function reads — a locale, a tenant ID, anything scoped at the root layout.\u003C\u002Fp>",{"id":195,"html":196,"type":83},"b29","\u003Cp>\u003Cstrong>Step 5 — the fix.\u003C\u002Fstrong> 16.4.5 closes the race itself, so \u003Ccode>unstable_cache()\u003C\u002Fcode> can no longer join an in-flight \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> fill and sidestep the rejection. The officially stated workaround, if you can&#39;t upgrade immediately, is the one you should be doing anyway: \u003Cstrong>never nest \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> inside \u003Ccode>unstable_cache()\u003C\u002Fcode>.\u003C\u002Fstrong> Outside the race window, that nesting already throws — so simply not writing the pattern removes the exposure entirely, upgrade or not.\u003C\u002Fp>",{"id":198,"html":199,"text":199,"type":91,"level":31},"b30","Edge cases",{"id":201,"type":97,"items":202,"ordered":18},"b31",[203,204,205,206],"\u003Cstrong>This needs a process restart to matter.\u003C\u002Fstrong> The race is specifically about the \u003Cem>first\u003C\u002Fem> 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.","\u003Cstrong>&quot;It throws normally, so I&#39;m safe&quot; was the trap.\u003C\u002Fstrong> Teams who&#39;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 &quot;this is rejected in the common case&quot; and &quot;this is rejected in every case.&quot;","\u003Cstrong>It doesn&#39;t require the function to look dangerous.\u003C\u002Fstrong> \u003Ccode>getGreeting\u003C\u002Fcode> above reads no secrets and no per-user data — it&#39;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 \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function reads it through \u003Ccode>next\u002Froot-params\u003C\u002Fcode>, it&#39;s part of that function&#39;s real identity, whether or not the code wrapping it knows that.","\u003Cstrong>\u003Ccode>keyParts\u003C\u002Fcode> would not have fixed this.\u003C\u002Fstrong> You might reach for \u003Ccode>unstable_cache\u003C\u002Fcode>&#39;s \u003Ccode>keyParts\u003C\u002Fcode> array to manually add disambiguating identifiers. That helps for its \u003Cem>own\u003C\u002Fem> arguments, but it doesn&#39;t change the underlying problem: nesting a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function inside \u003Ccode>unstable_cache()\u003C\u002Fcode> is unsupported regardless of how carefully you key the outer call.",{"id":208,"html":209,"text":209,"type":91,"level":31},"b32","Best practices",{"id":211,"type":97,"items":212,"ordered":18},"b33",[213,214,215,216,217],"\u003Cstrong>Upgrade now.\u003C\u002Fstrong> \u003Ccode>next@16.4.5\u003C\u002Fcode> closes the race outright. This is the one unambiguous action item in this whole article.","\u003Cstrong>Grep your codebase for \u003Ccode>unstable_cache(\u003C\u002Fcode> wrapping a function that also carries \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>.\u003C\u002Fstrong> That pattern is the exposure, full stop — remove the nesting even after upgrading, since it was never a supported combination.","\u003Cstrong>Treat \u003Ccode>unstable_cache()\u003C\u002Fcode> as legacy going forward.\u003C\u002Fstrong> The current docs open its reference page with a note that \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> replaces it for most use cases in Next.js 16 and point you at Cache Components — \u003Ccode>unstable_cache\u003C\u002Fcode> remains supported mainly for non-\u003Ccode>fetch\u003C\u002Fcode> data that must persist across deployments. New code shouldn&#39;t reach for it.","\u003Cstrong>When you do need caching outside a render path\u003C\u002Fstrong> — a webhook, a cron job, a route handler — call your data-fetching function directly and cache \u003Cem>that\u003C\u002Fem>, rather than wrapping an existing \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function meant for render-time use.","\u003Cstrong>Know what&#39;s actually in your cache key before you ship a cached function.\u003C\u002Fstrong> This bug is a specific instance of a general rule: a cache key that omits something your function&#39;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.",{"id":219,"html":220,"text":220,"type":91,"level":31},"b34","FAQ",{"id":222,"html":223,"text":223,"type":91,"level":43},"b35","Do I need to do anything beyond upgrading?",{"id":225,"html":226,"type":83},"b36","\u003Cp>Upgrading to 16.4.5 closes the race, which is the actual vulnerability. (Root params, and therefore this exact bug, don&#39;t exist before Next.js 16.3 — there&#39;s no 15.x line to patch.) On top of that, removing any \u003Ccode>unstable_cache(\u003C\u002Fcode> wrapper around a \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function is good hygiene regardless of version, since that nesting was never a supported pattern.\u003C\u002Fp>",{"id":228,"html":229,"text":230,"type":91,"level":43},"b37","Does this affect apps that don&#39;t use root params at all?","Does this affect apps that don't use root params at all?",{"id":232,"html":233,"type":83},"b38","\u003Cp>The specific CVE is about root-param identity going missing from a cache key. If your \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> functions never read a root param (no \u003Ccode>next\u002Froot-params\u003C\u002Fcode> imports), this exact mechanism doesn&#39;t apply to your cache keys — but the broader nesting pattern was still unsupported and still worth removing.\u003C\u002Fp>",{"id":235,"html":236,"text":237,"type":91,"level":43},"b39","Is \u003Ccode>unstable_cache()\u003C\u002Fcode> being removed?","Is unstablecache() being removed?",{"id":239,"html":240,"type":83},"b40","\u003Cp>Not in this release, and the advisory doesn&#39;t say that. But the current docs open the \u003Ccode>unstable_cache\u003C\u002Fcode> reference page with a note that \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> replaces it for most use cases in Next.js 16, pointing you at Cache Components instead. Write new code against \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>.\u003C\u002Fp>",{"id":242,"html":243,"text":243,"type":91,"level":43},"b41","Does the Pages Router hit this?",{"id":245,"html":246,"type":83},"b42","\u003Cp>No — root params, \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>, and this specific race are all App Router concepts. The Pages Router has no root-param mechanism for this bug to involve.\u003C\u002Fp>",{"id":248,"html":249,"text":249,"type":91,"level":43},"b43","How severe is this, really?",{"id":251,"html":252,"type":83},"b44","\u003Cp>The advisory rates it Moderate, CVSS v4 base score 6.3 — network-exploitable, low complexity, but with &quot;attack requirements present&quot; (the cold-start timing window) and only a low confidentiality impact scored at the framework level. Whether it&#39;s more serious \u003Cem>for your app\u003C\u002Fem> depends entirely on what your root-param-scoped \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> functions return.\u003C\u002Fp>",{"id":254,"html":255,"text":255,"type":91,"level":43},"b45","What exactly triggers the race?",{"id":257,"html":258,"type":83},"b46","\u003Cp>An \u003Ccode>unstable_cache()\u003C\u002Fcode> call arriving while the wrapped \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> function&#39;s very first fill after a process start is still in flight from a concurrent direct call. It&#39;s a timing condition tied to process startup, not something a request can reliably force on demand.\u003C\u002Fp>",{"id":260,"html":261,"text":261,"type":91,"level":31},"b47","Cheat sheet",{"id":263,"code":264,"type":130,"language":265,"highlight":266},"b48","Next.js cache key composition — what's actually in each one:\n\n\"use cache\" function key:\n  buildId + functionId (hash of location + signature)\n  + serializable arguments\n  + closed-over variables (captured automatically)\n  + ONLY the root params this function actually reads (via next\u002Froot-params)\n\nunstable_cache() call key:\n  function arguments + stringified function\n  + optional keyParts[] you pass explicitly\n  + NOTHING about root params — it has no mechanism to track them\n\nThe vulnerable pattern (don't write this, patched or not):\n  async function getData() {\n    \"use cache\"\n    const val = await someRootParam()   \u002F\u002F part of THIS function's real identity\n    return ...\n  }\n  const wrapped = unstable_cache(getData, [...])  \u002F\u002F unstable_cache can't see that identity\n\nGHSA-mfr8-8qww-v76h \u002F CVE-2026-119790\n  Affected:  next >= 16.3.0, \u003C= 16.4.4\n  Patched:   16.4.5\n  Note:      root params don't exist before 16.3, so the 15.x line was never affected\n  Severity:  Moderate, CVSS v4 6.3\n  Root cause: cold-start race let unstable_cache() join an in-flight \"use cache\"\n              fill before the \"unsupported nesting\" check could reject it\n  Workaround (any version): never nest \"use cache\" inside unstable_cache()\n\nMigration direction: unstable_cache() → \"use cache\" (the docs recommend \"use cache\" \u002F\nCache Components for most use cases as of Next.js 16)","plain",[],{"id":268,"html":269,"type":83},"b49","\u003C!-- playground:start -->",{"id":271,"html":272,"text":272,"type":91,"level":31},"b50","🎮 Try it yourself",{"id":274,"html":275,"type":83},"b51","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-unstable-cache-root-params-leak\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":277,"html":278,"type":83},"b52","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":280,"html":281,"type":83},"b53","\u003C!-- playground:end -->",{"id":283,"html":284,"text":284,"type":91,"level":31},"b54","Key takeaways",{"id":286,"type":97,"items":287,"ordered":18},"b55",[288,289,290,291],"\u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> and \u003Ccode>unstable_cache()\u003C\u002Fcode> build their cache keys from genuinely different inputs — one tracks root params automatically, the other has no concept of them at all.","Nesting \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> inside \u003Ccode>unstable_cache()\u003C\u002Fcode> is unsupported and normally throws — but a narrow cold-start race in versions 16.3.0–16.4.4 let an \u003Ccode>unstable_cache()\u003C\u002Fcode> 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 \u003Ccode>unstable_cache()\u003C\u002Fcode> as the legacy API the docs already say it is.","The general lesson outlives this specific CVE: a cache key that&#39;s missing something your function&#39;s output depends on is a correctness bug first, and sometimes a security bug second — know exactly what&#39;s in yours.",{"id":293,"html":294,"type":83},"b56","\u003Cp>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.\u003C\u002Fp>",{"id":296,"html":297,"type":83},"b57","\u003C!-- quiz:start -->",{"id":299,"html":300,"text":300,"type":91,"level":31},"b58","🧠 Test yourself",{"id":302,"html":303,"type":83},"b59","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-unstable-cache-root-params-leak\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":305,"html":306,"type":83},"b60","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":308,"html":309,"type":83},"b61","\u003C!-- quiz:end -->",{"id":311,"html":312,"type":83},"b62","\u003C!-- related:start -->",{"id":314,"html":315,"text":315,"type":91,"level":31},"b63","📚 Read next",{"id":317,"type":97,"items":318,"ordered":18},"b64",[319,320,321],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained\">Next.js Cache Components Explained (with Cheat Sheet)\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag\">Next.js 16 updateTag vs revalidateTag: What Changed\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output\">Next.js 16.4 ensureStatic: Locking Routes Fully Static\u003C\u002Fa>",{"id":323,"html":324,"type":83},"b65","\u003C!-- related:end -->",{"id":326,"type":327},"b66","divider",{"id":329,"html":330,"type":83},"b67","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":332,"html":333,"type":83},"b68","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":335,"type":97,"items":336,"ordered":18},"b69",[337,338,339],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","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](https:\u002F\u002Fgithub.com\u002Fvercel\u002Fnext.js\u002Fsecurity\u002Fadvisories\u002FGHSA-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.\n\nThis 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.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- 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\n- Explain what composes an `unstable_cache()` key, and why it's a fundamentally different (and narrower) idea than `\"use cache\"`'s\n- Reproduce, conceptually, why nesting `\"use cache\"` inside `unstable_cache()` is unsupported — and why that safety net still had a hole\n- Audit your own app for the vulnerable pattern and know exactly what to do about it\n- Tell whether you should still be reaching for `unstable_cache()` at all in new code\n\n**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](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob) covers the basics first — worth reading before this one, not required.\n\n## Contents\n\n- [The bug, shown wrong-way-first](#the-bug-shown-wrong-way-first)\n- [The mental model: two caches, two ideas of identity](#the-mental-model-two-caches-two-ideas-of-identity)\n- [Staged walkthrough: how the leak actually happened](#staged-walkthrough-how-the-leak-actually-happened)\n- [Edge cases](#edge-cases)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The bug, shown wrong-way-first\n\nSay you run a docs site with a locale segment in the root layout, and a cached function that greets the reader in their language:\n\n```tsx\n\u002F\u002F app\u002F[locale]\u002Flayout.tsx — locale is a ROOT param: it comes from the root layout's segment\nimport { locale } from \"next\u002Froot-params\"\n\nasync function getGreeting() {\n  \"use cache\"\n  const loc = await locale()\n  return GREETINGS[loc] \u002F\u002F { en: \"Hello\", fr: \"Bonjour\", de: \"Hallo\" }\n}\n```\n\nRendered 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.\n\nThe 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:\n\n```ts\n\u002F\u002F some route handler, written by someone who just wanted \"the cached greeting\"\nimport { unstable_cache } from \"next\u002Fcache\"\n\nconst getCachedGreeting = unstable_cache(getGreeting, [\"greeting\"])\n```\n\nThis 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.\n\n## The mental model: two caches, two ideas of identity\n\nThe 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.\n\n**A `\"use cache\"` function's key is built from:**\n\n- A **build ID**, unique per deploy — change the build, invalidate everything.\n- A **function ID**, a hash of the function's location and signature in your code.\n- Its **serializable arguments**, if any.\n- 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.\n- **Only the root params it actually reads.** Root params are the dynamic segments of the *root* layout (your `locale` in `app\u002F[locale]\u002Flayout.tsx`, say). They're read through imported getter functions from `next\u002Froot-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 calls `locale()` doesn't get `locale` in its key at all; one that does, does.\n\n**An `unstable_cache()` call's key is built from:**\n\n- Its **function's arguments**, plus a **stringified version of the function itself**.\n- An optional **`keyParts`** array you can pass to add more identifiers by hand.\n- **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.\n\nPut 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.\n\nNormally 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.\n\n## Staged walkthrough: how the leak actually happened\n\n**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.\n\n**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**.\n\n**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.\n\n**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.\n\n**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.\n\n## Edge cases\n\n- **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.\n- **\"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.\"\n- **It doesn't require the function to look dangerous.** `getGreeting` above 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 through `next\u002Froot-params`, it's part of that function's real identity, whether or not the code wrapping it knows that.\n- **`keyParts` would not have fixed this.** You might reach for `unstable_cache`'s `keyParts` array to manually add disambiguating identifiers. That helps for its *own* arguments, but it doesn't change the underlying problem: nesting a `\"use cache\"` function inside `unstable_cache()` is unsupported regardless of how carefully you key the outer call.\n\n## Best practices\n\n- **Upgrade now.** `next@16.4.5` closes the race outright. This is the one unambiguous action item in this whole article.\n- **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.\n- **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_cache` remains supported mainly for non-`fetch` data that must persist across deployments. New code shouldn't reach for it.\n- **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.\n- **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.\n\n## FAQ\n\n### Do I need to do anything beyond upgrading?\n\nUpgrading 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.\n\n### Does this affect apps that don't use root params at all?\n\nThe 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\u002Froot-params` imports), this exact mechanism doesn't apply to your cache keys — but the broader nesting pattern was still unsupported and still worth removing.\n\n### Is `unstable_cache()` being removed?\n\nNot 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\"`.\n\n### Does the Pages Router hit this?\n\nNo — 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.\n\n### How severe is this, really?\n\nThe 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.\n\n### What exactly triggers the race?\n\nAn `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.\n\n## Cheat sheet\n\n```\nNext.js cache key composition — what's actually in each one:\n\n\"use cache\" function key:\n  buildId + functionId (hash of location + signature)\n  + serializable arguments\n  + closed-over variables (captured automatically)\n  + ONLY the root params this function actually reads (via next\u002Froot-params)\n\nunstable_cache() call key:\n  function arguments + stringified function\n  + optional keyParts[] you pass explicitly\n  + NOTHING about root params — it has no mechanism to track them\n\nThe vulnerable pattern (don't write this, patched or not):\n  async function getData() {\n    \"use cache\"\n    const val = await someRootParam()   \u002F\u002F part of THIS function's real identity\n    return ...\n  }\n  const wrapped = unstable_cache(getData, [...])  \u002F\u002F unstable_cache can't see that identity\n\nGHSA-mfr8-8qww-v76h \u002F CVE-2026-119790\n  Affected:  next >= 16.3.0, \u003C= 16.4.4\n  Patched:   16.4.5\n  Note:      root params don't exist before 16.3, so the 15.x line was never affected\n  Severity:  Moderate, CVSS v4 6.3\n  Root cause: cold-start race let unstable_cache() join an in-flight \"use cache\"\n              fill before the \"unsupported nesting\" check could reject it\n  Workaround (any version): never nest \"use cache\" inside unstable_cache()\n\nMigration direction: unstable_cache() → \"use cache\" (the docs recommend \"use cache\" \u002F\nCache Components for most use cases as of Next.js 16)\n```\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-unstable-cache-root-params-leak\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Key takeaways\n\n- `\"use cache\"` and `unstable_cache()` build their cache keys from genuinely different inputs — one tracks root params automatically, the other has no concept of them at all.\n- Nesting `\"use cache\"` inside `unstable_cache()` is unsupported and normally throws — but a narrow cold-start race in versions 16.3.0–16.4.4 let an `unstable_cache()` call slip past that check and inherit a result keyed without the root param that actually identified it.\n- 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.\n- 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.\n\nA 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.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-unstable-cache-root-params-leak\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Next.js Cache Components Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained)\n- [Next.js 16 updateTag vs revalidateTag: What Changed](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag)\n- [Next.js 16.4 ensureStatic: Locking Routes Fully Static](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":342,"description":49},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-unstable-cache-root-params-leak","01a1b128-0d79-701d-bb68-757d11f05775",{"name":345,"part":346,"total":346,"items":347},"Next.js Deep Dive",11,[348,353,358,362,367,371,375,379,383,387,391],{"slug":349,"title":350,"publishedAt":351,"readingMinutes":352},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",14,{"slug":354,"title":355,"publishedAt":356,"readingMinutes":357},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",15,{"slug":359,"title":360,"publishedAt":361,"readingMinutes":352},"nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right","2026-09-15T17:50:47.218Z",{"slug":363,"title":364,"publishedAt":365,"readingMinutes":366},"nextjs-weekly-middleware-to-proxy-network-boundary","Next.js proxy.ts Explained (with Cheat Sheet)","2026-09-22T19:36:08.416Z",12,{"slug":368,"title":369,"publishedAt":370,"readingMinutes":357},"nextjs-weekly-route-handlers-caching-streaming","Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16","2026-09-29T09:16:08.176Z",{"slug":372,"title":373,"publishedAt":374,"readingMinutes":352},"nextjs-weekly-streaming-metadata-head","Next.js Streaming Metadata: Why Your `\u003Chead>` Looks Incomplete","2026-10-06T06:10:31.468Z",{"slug":376,"title":377,"publishedAt":378,"readingMinutes":357},"nextjs-weekly-image-priority-quality-changes","Next.js 16 Image: priority Deprecated, quality Now Allowlisted","2026-10-13T06:10:29.815Z",{"slug":380,"title":381,"publishedAt":382,"readingMinutes":352},"nextjs-weekly-instant-navigations-partial-prefetching","Next.js 16.3 Instant Navigations: How Partial Prefetching Works","2026-10-20T06:10:59.936Z",{"slug":384,"title":385,"publishedAt":386,"readingMinutes":352},"nextjs-weekly-updatetag-vs-revalidatetag","Next.js 16 updateTag vs revalidateTag: What Changed","2026-10-27T06:11:02.727Z",{"slug":388,"title":389,"publishedAt":390,"readingMinutes":51},"nextjs-weekly-ensurestatic-locks-static-output","Next.js 16.4 ensureStatic: Locking Routes Fully Static","2026-11-03T06:10:26.449Z",{"slug":46,"title":47,"publishedAt":52,"readingMinutes":51},{"id":393,"locked":18},"01a1b128-0da8-73eb-a897-6c89ed77c0d2",[395],{"id":396,"slug":46,"title":397,"_count":398},"01a1b128-0dc8-7169-a64e-ba4827e0c767","Next.js unstable_cache Cache-Key Leak",{"questions":399},8,[401],{"locale":13,"slug":46},{"id":396,"slug":46,"title":397,"_count":403,"questionCount":399},{"questions":399},[405,409,413,417,419,423,427,431,435,439,443,447],{"slug":406,"name":407,"articles":408},"webdev","Webdev",174,{"slug":410,"name":411,"articles":412},"javascript","Javascript",141,{"slug":414,"name":415,"articles":416},"frontend","Frontend",96,{"slug":72,"name":73,"articles":418},71,{"slug":420,"name":421,"articles":422},"css","Css",51,{"slug":424,"name":425,"articles":426},"performance","Performance",30,{"slug":428,"name":429,"articles":430},"typescript","Typescript",25,{"slug":432,"name":433,"articles":434},"react","React",20,{"slug":436,"name":437,"articles":438},"programming","Programming",19,{"slug":440,"name":441,"articles":442},"browser","Browser",18,{"slug":444,"name":445,"articles":446},"node","Node",16,{"slug":448,"name":449,"articles":366},"html","Html",[],{"items":452,"meta":556},[453,464,479,494,509,524,541],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":399,"likeCount":19,"commentCount":19,"author":454,"vertical":455,"topic":456,"tags":457,"_count":462,"playground":463,"hasQuiz":17,"hasPlayground":17},{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[458,459,460,461],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":46},{"id":465,"slug":388,"title":389,"subtitle":48,"excerpt":466,"coverUrl":467,"locale":13,"readingMinutes":51,"publishedAt":390,"viewCount":468,"likeCount":19,"commentCount":19,"author":469,"vertical":470,"topic":471,"tags":472,"_count":477,"playground":478,"hasQuiz":17,"hasPlayground":17},"01a1972e-c476-72af-9154-03327c89a57d","Next.js 16.4 stabilizes ensureStatic: lock a route's shell, prefetch, or full navigation to static output at build time, with a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-ensurestatic-locks-static-output.png",202,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[473,474,475,476],{"slug":61,"name":62,"color":48},{"slug":69,"name":70,"color":48},{"slug":406,"name":407,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":388},{"id":480,"slug":384,"title":385,"subtitle":48,"excerpt":481,"coverUrl":482,"locale":13,"readingMinutes":352,"publishedAt":386,"viewCount":483,"likeCount":19,"commentCount":19,"author":484,"vertical":485,"topic":486,"tags":487,"_count":492,"playground":493,"hasQuiz":17,"hasPlayground":17},"01a1749f-19a5-768a-82af-f3b8236ad42b","Next.js 16 deprecated revalidateTag's single-argument call and added updateTag. What the new profile argument changes, plus a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-updatetag-vs-revalidatetag.png",311,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[488,489,490,491],{"slug":61,"name":62,"color":48},{"slug":69,"name":70,"color":48},{"slug":406,"name":407,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":384},{"id":495,"slug":380,"title":381,"subtitle":48,"excerpt":496,"coverUrl":497,"locale":13,"readingMinutes":352,"publishedAt":382,"viewCount":498,"likeCount":19,"commentCount":19,"author":499,"vertical":500,"topic":501,"tags":502,"_count":507,"playground":508,"hasQuiz":17,"hasPlayground":17},"01a149cb-68e6-773f-bbbb-fce803732119","Next.js 16.3 shipped Instant Navigations: Partial Prefetching plus the App Shell. Learn the model, the Suspense gotcha, and a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-instant-navigations-partial-prefetching.png",438,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[503,504,505,506],{"slug":61,"name":62,"color":48},{"slug":406,"name":407,"color":48},{"slug":72,"name":73,"color":48},{"slug":410,"name":411,"color":48},{"assessments":75},{"slug":380},{"id":510,"slug":376,"title":377,"subtitle":48,"excerpt":511,"coverUrl":512,"locale":13,"readingMinutes":357,"publishedAt":378,"viewCount":513,"likeCount":19,"commentCount":19,"author":514,"vertical":515,"topic":516,"tags":517,"_count":522,"playground":523,"hasQuiz":17,"hasPlayground":17},"01a120b9-d611-74ef-ad15-a267677c3251","Next.js 16 quietly deprecated Image's priority for preload and restricts quality to an allowlist (default: 75). What breaks, plus a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-image-priority-quality-changes.png",685,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[518,519,520,521],{"slug":61,"name":62,"color":48},{"slug":432,"name":433,"color":48},{"slug":406,"name":407,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":376},{"id":525,"slug":372,"title":373,"subtitle":48,"excerpt":526,"coverUrl":527,"locale":13,"readingMinutes":352,"publishedAt":374,"viewCount":528,"likeCount":19,"commentCount":19,"author":529,"vertical":530,"topic":531,"tags":532,"_count":539,"playground":540,"hasQuiz":17,"hasPlayground":17},"01a0fc86-ee8a-74df-9c72-1cb57c0ba6cc","Next.js 16 streaming metadata sends users a page before generateMetadata resolves, but blocks crawlers for a complete head. How it works, plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-streaming-metadata-head.png",661,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[533,534,537,538],{"slug":61,"name":62,"color":48},{"slug":535,"name":536,"color":48},"seo","Seo",{"slug":406,"name":407,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":372},{"id":542,"slug":368,"title":369,"subtitle":48,"excerpt":543,"coverUrl":544,"locale":13,"readingMinutes":357,"publishedAt":370,"viewCount":545,"likeCount":19,"commentCount":19,"author":546,"vertical":547,"topic":548,"tags":549,"_count":554,"playground":555,"hasQuiz":17,"hasPlayground":17},"01a0ec73-0927-7298-a1ae-88b0d2458ab0","Next.js 15 made GET Route Handlers dynamic by default. What that changes for code written for 14, how to cache one on purpose in 16, plus a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-route-handlers-caching-streaming.png",872,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[550,551,552,553],{"slug":61,"name":62,"color":48},{"slug":428,"name":429,"color":48},{"slug":406,"name":407,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":368},{"page":75,"perPage":53,"total":346,"totalPages":31},"\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002F[locale]\u002Flayout.tsx — locale is a ROOT param: it comes from the root layout's segment\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> locale \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"next\u002Froot-params\"\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getGreeting\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> loc \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">locale\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-num\">GREETINGS\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>loc\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F { en: \"Hello\", fr: \"Bonjour\", de: \"Hallo\" }\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F some route handler, written by someone who just wanted \"the cached greeting\"\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> unstable_cache \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"next\u002Fcache\"\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> getCachedGreeting \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">unstable_cache\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>getGreeting\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"greeting\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003Cdiv>24\u003C\u002Fdiv>\u003Cdiv>25\u003C\u002Fdiv>\u003Cdiv>26\u003C\u002Fdiv>\u003Cdiv>27\u003C\u002Fdiv>\u003Cdiv>28\u003C\u002Fdiv>\u003Cdiv>29\u003C\u002Fdiv>\u003Cdiv>30\u003C\u002Fdiv>\u003Cdiv>31\u003C\u002Fdiv>\u003Cdiv>32\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">Next.js cache key composition — what's actually in each one:\n\n\u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan> function key:\n  buildId + functionId (hash of location + signature)\n  + serializable arguments\n  + closed-over variables (captured automatically)\n  + ONLY the root params this function actually reads (via next\u002Froot-params)\n\nunstable_cache() call key:\n  function arguments + stringified function\n  + optional keyParts[] you pass explicitly\n  + NOTHING about root params — it has no mechanism to track them\n\nThe vulnerable pattern (don't write this, patched or not):\n  async function getData() {\n    \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan>\n    const val = await someRootParam()   \u002F\u002F part of THIS function's real identity\n    return ...\n  }\n  const wrapped = unstable_cache(getData, [...])  \u002F\u002F unstable_cache can't see that identity\n\nGHSA-mfr8-8qww-v76h \u002F CVE-2026-119790\n  Affected:  next &gt;= 16.3.0, &lt;= 16.4.4\n  Patched:   16.4.5\n  Note:      root params don't exist before 16.3, so the 15.x line was never affected\n  Severity:  Moderate, CVSS v4 6.3\n  Root cause: cold-start race let unstable_cache() join an in-flight \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan>\n              fill before the \u003Cspan class=\"shj-str\">\"unsupported nesting\"\u003C\u002Fspan> check could reject it\n  Workaround (any version): never nest \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan> inside unstable_cache()\n\nMigration direction: unstable_cache() → \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan> (the docs recommend \u003Cspan class=\"shj-str\">\"use cache\"\u003C\u002Fspan> \u002F\nCache Components for most use cases as of Next.js 16)\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":561},[]]