
Next.js use cache: private
Caching and personalization usually pull in opposite directions. Caching wants one answer you can hand out to everyone; personalization wants an answer that depends on who's asking. Next.js's Cache Components model gives you 'use cache' for the first case, but the moment a cached function needs to read a cookie, a header, or a search param to personalize its result, 'use cache' refuses outright — those runtime request APIs aren't allowed inside it, because a value cached from one visitor's request could otherwise leak into another visitor's response.
'use cache: private' is the directive built for exactly that gap. It lets a function read request-scoped data like cookies(), headers(), and searchParams, and still get a caching layer around it — but with a deliberate, important constraint: nothing gets stored on the server. Whatever this directive caches lives only in the requesting browser's memory, for that one visitor, and vanishes the moment the page reloads. Understanding that constraint is the whole point of this article, because it's the thing most people misjudge the first time they reach for it.
The problem this directive solves
Imagine a product page that shows personalized recommendations based on the visitor's session. The recommendations are expensive to compute — maybe they hit a recommendation engine or run a database query joining purchase history against a similarity model. You'd like to cache that work so a visitor doesn't pay the full cost on every render of the same page. But the input to that computation is a session ID pulled from a cookie, and the whole point is that different visitors get different answers.
Two options exist inside the Cache Components model:
- Refactor so the function that reads
cookies()lives outside the cached function, and you pass the resolved session ID in as a plain argument. The cached function itself becomes pure — same input, same output — which is exactly what ordinary'use cache'is designed for. - Reach for
'use cache: private'when that refactor genuinely isn't practical — the runtime access is buried deep in a call chain, wrapped in a third-party client, or otherwise awkward to hoist out.
The docs are explicit that option 1 is the default recommendation, and option 2 is the escape hatch. There's also a second, less obvious reason to reach for it: compliance. If a regulation or internal policy says certain data can't be persisted on your servers even temporarily, a private cache that only ever exists in the visitor's own browser sidesteps that requirement entirely — there's no server-side artifact to worry about retention rules for.
What "never stored on the server" actually means
This is the detail that trips people up, so it's worth stating plainly: a 'use cache: private' result is cached in the browser's memory, not in Next.js's server-side cache store, not in your CDN, and not in whatever custom cache handler you might have configured for regular 'use cache' functions. Two direct consequences follow from that:
- It does not survive a page reload. Navigate away and back, or hard-refresh, and the function runs fresh. This is not a durable cache in the sense that
'use cache'or ISR are — think of it more like a per-session memoization that only helps with client-side navigations within a single visit. - You cannot configure a custom cache handler for it. The docs state this directly — whatever storage backend you've wired up for your regular cached functions (Redis, a CDN-backed remote cache, whatever) is irrelevant here. There's no handler to configure because there's no server-side store to configure it for.
If you came into this expecting a per-user cache that persists across sessions or across server instances, this isn't that. It's much closer to "avoid recomputing this while the visitor is actively navigating around" than "avoid recomputing this ever."
Enabling the directive
'use cache: private' is part of the Cache Components feature, so it requires the cacheComponents flag turned on in your config:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
/** @type {import('next').NextConfig} */
const nextConfig = {
cacheComponents: true,
};
module.exports = nextConfig;
Without this flag enabled project-wide, the directive isn't available at all — it's not something you can opt into on a per-function basis independently of the rest of your app's caching model.
A worked example
Here's the canonical shape: a product page that renders instantly (its core content doesn't depend on the visitor), with a personalized recommendations panel streamed in separately via Suspense.
import { Suspense } from "react";
import { cookies } from "next/headers";
import { cacheLife, cacheTag } from "next/cache";
export async function generateStaticParams() {
return [{ id: "1" }];
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<div>
<ProductDetails id={id} />
<Suspense fallback={<div>Loading recommendations...</div>}>
<Recommendations productId={id} />
</Suspense>
</div>
);
}
async function Recommendations({ productId }: { productId: string }) {
const recommendations = await getRecommendations(productId);
return (
<div>
{recommendations.map((rec) => (
<ProductCard key={rec.id} product={rec} />
))}
</div>
);
}
async function getRecommendations(productId: string) {
"use cache: private";
cacheTag(`recommendations-${productId}`);
cacheLife({ stale: 60 });
// Access cookies within private cache functions
const sessionId = (await cookies()).get("session-id")?.value || "guest";
return getPersonalizedRecommendations(productId, sessionId);
}
A few things worth noticing that the code alone doesn't spell out:
The directive lives on the function, not the component. getRecommendations is a plain async function, not the Recommendations component itself. This matters because it means you can wrap just the narrow piece of logic that needs runtime data, while everything around it — the component tree, the Suspense boundary, the rest of the page — stays outside that constraint.
cacheTag and cacheLife still apply. Even though nothing is persisted server-side, you still configure a cacheLife policy. That's not decorative — it directly controls behavior on the client side, which is the next section's subject.
The Suspense boundary is doing real work. Because this function touches cookies(), it can never run during static shell generation — Next.js excludes it from prerendering entirely, and it executes fresh on every server render. Wrapping it in Suspense lets the rest of the page (ProductDetails) stream out immediately while this personalized slice resolves separately, rather than blocking the whole page on a per-request computation.
Request APIs: what's allowed and what isn't
'use cache: private' widens what's allowed compared to plain 'use cache', but it doesn't open every door:
| API | Allowed in use cache | Allowed in use cache: private |
|---|---|---|
cookies() | No | Yes |
headers() | No | Yes |
searchParams | No | Yes |
connection() | No | No |
connection() staying off-limits in both cases is worth pausing on. It's not an oversight — connection() exposes information specific to the individual network connection, which is a level below "this visitor's session" and arguably not something any caching layer, however private, should be pretending to memoize. If your code needs connection(), that logic has to live outside any cached function, private or not.
The cacheLife timing requirements you can't skip
The docs call out two thresholds that aren't obvious from the API surface alone:
- The
staletime in yourcacheLifeconfig must be at least 30 seconds for per-link prefetching to work correctly. - It must be at least 5 minutes for the private-cached content to be eligible for inclusion in the route's App Shell (the static portion of the page that gets served instantly, before any per-request work resolves).
If you set cacheLife({ stale: 5 }) because the underlying data changes every few seconds, you'll get correct behavior in the narrow sense — no stale data — but you'll silently lose prefetching benefits and App Shell inclusion for that content. This is the kind of thing that's easy to miss because nothing throws an error; the page just quietly performs worse than it could.
How this compares to the other cache directives
Next.js's Cache Components model gives you three closely related but distinct directives, and mixing them up is a common source of confusion:
| Directive | Server-side storage | Runtime API access | Typical use |
|---|---|---|---|
use cache | Yes (shared across all visitors) | No | Data that's the same for everyone — a blog post, a product catalog page |
use cache: private | No (browser memory only) | cookies(), headers(), searchParams | Personalized results derived from a session, where refactoring the runtime read out isn't practical |
use cache: remote | Yes (persistent, shared) | No | Same as use cache, but explicitly backed by a remote cache handler rather than in-memory |
The naming is deliberately parallel — all three share the use cache prefix precisely because they're variations on one idea (cache this function's output) with different rules about where the data can come from and where the result can live. If you're reaching for 'use cache: private' because you assumed it was just "the version of use cache that also does server-side storage but scoped per-user," that's the wrong mental model — that's not a feature Cache Components currently offers. Server-side per-user caching still means writing your own logic (a keyed lookup in Redis by session ID, for instance) outside the directive system entirely.
When you shouldn't reach for this
Given how easy it is to add 'use cache: private' to any function with a runtime read buried in it, it's worth being explicit about when not to:
- When the refactor to hoist the runtime read out is actually straightforward. If
cookies()is called at the top of your Server Component and the value could just be passed down as a prop to a plainuse cachefunction, do that instead — you get server-side, cross-visitor caching, which is strictly more valuable than per-browser memoization. - When you need the result to survive a page reload. It won't. If durability matters, you need a real persistence layer (a session store, a database-backed cache, a signed cookie holding the computed result itself) — not this directive.
- When the personalized computation is cheap anyway. Caching adds a small amount of bookkeeping overhead. If
getRecommendationsabove were a single indexed database lookup, the caching layer might not be earning its keep, especially given it only helps within a single browser session's lifetime.
Key takeaways
| Question | Answer |
|---|---|
| Where is the result stored? | In the browser's memory only — never on the server |
| Does it survive a page reload? | No |
| Which runtime APIs can it access? | cookies(), headers(), searchParams |
| Which runtime API is still off-limits? | connection() |
| Can you use a custom cache handler? | No — there's no server-side store to configure one for |
Minimum stale time for prefetching | 30 seconds |
Minimum stale time for App Shell inclusion | 5 minutes |
| Requires which config flag? | cacheComponents: true |
'use cache: private' exists for a narrow, specific situation: a function that both needs to read request-scoped data and would benefit from not recomputing that read on every single render within a visit. It's not a general-purpose personalization cache, and it's not a substitute for server-side per-user storage. Reach for it when hoisting the runtime read out of the cached function isn't practical, or when compliance rules mean you specifically don't want server-side persistence — and reach for a real persistence layer instead when you do need the result to last longer than one browser session.


