
Fix Use Cache Private Cookies - Confirmed Next.js 16 Bug
Skilldham
Engineering deep-dives for developers who want real understanding.
Last updated: August 2026
TL;DR
use cache: private stops caching after a client-side navigation in Next.js 16. This happens once cookies() is read. It's a confirmed, team-tracked bug. It is not a config mistake. Two fixes work today. Force a hard navigation boundary. Or drop private and scope the cache key yourself with revalidateTag.
You wire up use cache: private on a function that reads cookies(). You test it. First load, it caches fine.
Then you click a link inside your app. Same route. Same user. Same cookie.
It refetches. Every time.
You check the docs again. You re-read the Cache Components guide. Everything says this should work - cache scoped per session, keyed off the cookie.
You start Googling "next.js use cache private cookies not working." You land on a GitHub issue. It has no fix. It's been open for months.
Here's what's actually happening. And the workaround that keeps this cached until Vercel ships a real fix.
What "Use Cache Private" Is Supposed to Do
use cache: private is part of Next.js 16's Cache Components model. It lets you cache a function's output per user, using cookies() as the scoping signal.
The idea is simple. A logged-in user hits a dashboard. You cache the expensive fetch behind their session cookie. The next request from that same user gets the cached value.

The Basic Setup
javascript
// Correct: intended usage per Next.js docs
import { cookies } from "next/headers";
export async function getDashboardData() {
"use cache";
cacheLife("private");
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
const data = await fetchDashboard(sessionId);
return data;
}On a full page load, this works exactly as documented. The cache holds. The problem shows up somewhere else entirely.
The Exact Failure - Reproducing the Bug
Load the dashboard route directly in the browser. The function runs once, caches, and subsequent full reloads hit the cache.
Now navigate to the route using a client-side <Link> from another page in the same app.
What Breaks
javascript
// Wrong: this looks fine but triggers the bug
export default function Nav() {
return (
<Link href="/dashboard">Dashboard</Link>
);
}Click that link. The cached function reruns. Check your server logs. The "expensive fetch" line fires again. Nothing about the cookie or the user changed.
Refresh the page directly and the cache works again. Navigate to it client-side a second time and it breaks again.
This is not a one-off. A separate community repro confirms the same failure with a different codebase. It is not a misconfiguration on your end.
Confirming It's a Known Issue
The Next.js team has this labeled and tracked. It has stayed open since November 2025 with no fix shipped as of this post. Your repro might match this pattern. A working full page load. A broken client-side navigation. If so, you're hitting this exact bug - not a new one.
Why It Breaks - Router Cache vs Function-Level Cache
Two caching systems are involved here. They were not built to talk to each other cleanly.
use cache: private is a function-level cache. It runs on the server and keys itself off the cookie value at call time.
Router Cache is a client-side cache. It stores rendered segments in the browser. This happens after a client-side navigation. Next.js then skips re-requesting them from the server.
The Actual Interaction
On a full page load, the server runs your function fresh. It reads the cookie and populates the private cache correctly.
On a client-side navigation, Router Cache intercepts the request first. This happens before the private cache scoping runs. The cookie never gets resolved the same way. The function reruns as if the private cache key never matched.
The docs describe the function-level behavior correctly. They do not cover what happens next. Router Cache sits in front of it during client navigation. That gap explains why AI tools get this wrong. They read the documentation alone. They describe the intended behavior. They miss the router interaction that actually breaks it.
The Workaround That Actually Works
Two options fix this today. Pick based on how much you can change about the route.
Option 1 - Force a Hard Navigation Boundary
javascript
// Correct: forces a full request instead of a Router Cache hit
import { redirect } from "next/navigation";
export default function DashboardLink() {
return (
<a href="/dashboard" onClick={(e) => {
e.preventDefault();
window.location.assign("/dashboard");
}}>
Dashboard
</a>
);
}This trades client-side navigation speed for correct caching. Use it only on the specific link that leads into the broken route, not app-wide.
Option 2 - Drop Private, Scope the Key Yourself
javascript
// Correct: manual session scoping avoids the private cache path entirely
import { cookies } from "next/headers";
import { unstable_cacheTag as cacheTag } from "next/cache";
export async function getDashboardData() {
"use cache";
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
cacheTag(`dashboard-${sessionId}`);
const data = await fetchDashboard(sessionId);
return data;
}This skips the private cache life entirely. You tag the cache manually with the session id. Call revalidateTag when the data changes. It survives client-side navigation. It never depends on the broken interaction.
Option 2 is the one to reach for in production. It is slightly more code. But it does not silently break. It won't fail just because someone clicks a link instead of typing a URL.
Wrong Fixes Developers Try
Adding cacheLife with a longer duration. This does not touch the Router Cache interaction. The function still reruns on client navigation. It just reruns with a longer cache life. That longer life never gets used.
Wrapping the component in Suspense. Suspense controls loading states, not cache key resolution. The fetch still fires twice.
Switching from cookies() to headers(). Same underlying Router Cache interaction applies. The cookie is not the part that is broken - the private cache path is.
How This Plays Out in Production
I hit this shipping Cache Components on Munshi. Dashboard data there is scoped per logged-in user, behind a session cookie. The first version used use cache: private exactly as documented, and it passed every direct-load test.
It broke in QA almost immediately. Someone navigated to the dashboard from the app's bottom tab bar, instead of a fresh load. The fetch count in the server logs doubled for every client-side visit. That's what surfaced it before it reached production.
Switching to manual cacheTag scoping fixed it. This kept caching on the route (see Option 2 above).
Key Takeaways
use cache: private works correctly on full page loads but breaks on client-side navigation in Next.js 16
The root cause is Router Cache intercepting the request before the private cache key resolves against the cookie
This is a confirmed, team-labeled bug, not a configuration mistake
Forcing a hard navigation is a quick fix for a single problem link
Manually scoping the cache key with cacheTag and the session id is the production-safe fix
cacheLife duration, Suspense boundaries, and switching to headers() do not fix this
Test any use cache: private route by navigating to it with a client-side Link, not just a full reload
FAQ
Does this bug affect all use cache: private usage, or only routes that read cookies()?
It affects any function using private cache life that resolves its cache key from cookies(). If your cached function does not touch cookies, this specific bug does not apply.
Is this fixed in a later Next.js 16 patch release?
Not as of this post. The tracked issue remains open with no shipped fix. Check the linked GitHub issue for the current status before assuming it's resolved.
Will this also happen with prefetched links?
Yes. Next.js prefetches routes linked with <Link> by default, which uses the same Router Cache path. Prefetching does not avoid the bug - it can trigger it earlier.
Does using revalidateTag manually cause any downsides compared to private?
You lose the automatic per-cookie scoping that private provides. You become responsible for building the cache key. You also call revalidateTag when the data changes. It is more code, but it is reliable.
Can I disable Router Cache to work around this instead?
You can reduce Router Cache staleness times in next.config.js. But disabling it removes the navigation speed benefit app-wide, not just on the broken route. Scoping the cache key manually is the more targeted fix.
Does this happen in development mode, production builds, or both?
Both. This is not a dev-only artifact of hot reloading. Production builds on Vercel show the same doubled fetch behavior on client-side navigation.
Is there a way to detect this is happening without checking server logs?
Add a temporary console.log with a timestamp inside the cached function. Check if it logs on every client-side navigation. If it fires more than once per session, you're hitting this bug.
Should I avoid use cache: private entirely until this is fixed?
Not entirely. It still works fine for routes reached only by a full page load. A directly bookmarked page is a good example. For anything reachable through in-app navigation, use the manual cacheTag approach instead.
Where This Fits
This is part of the Next.js cluster on SkillDham. Builds timing out on Cache Components is a separate problem. See the Cache Components build OOM fix for that one. It covers a related failure in the same feature.
Not invalidating after a server action is a different bug. That specific fix covers the revalidation side of the same caching model.
See the tracked GitHub issue for the behavior use cache: private is supposed to have. It includes the Vercel team's own repro. It confirms this is not expected behavior.
Cache Components is still new. Gaps like this one show up fast. The documented behavior and the real Router Cache interaction don't always match. That mismatch only shows up once real users start clicking around, instead of loading pages fresh. If you hit this, the manual cacheTag scoping will hold until Vercel ships a fix upstream.