skip to content

'use cache' and Explicit Caching

Next is moving from caching by default to caching when you say so, with a directive you put on a function, component, or file. Interviewers ask about it to see whether you track the framework's direction and can explain opt-in versus opt-out caching.

part ofNext.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In a Next.js App Router app, a developer adds `'use cache'` to a function that calls `cookies()` to read the session and returns that user's dashboard data. What does Next.js do about that, and how should the code be restructured?

level: seniorimportance: must knowfreq 52%

answer

  1. cached output is shared across requests
  2. cookies and headers are request-scoped
  3. the framework errors, not warns
  4. read outside, pass in as arguments
  5. per-user work is often not worth caching

basics

~20 s

Next.js rejects it: request-time APIs such as cookies() and headers() cannot be called inside a 'use cache' function, because a cached entry is shared across requests and would hand one user's data to another. Read the session outside the cached function and pass the values you need in as arguments.

solid answer

~60 s

Next refuses the combination — reading `cookies()`, `headers()` or `searchParams` inside a `'use cache'` function is an error, not a silent behaviour. The reason is that a cached entry is computed once and replayed for every request whose key matches, so if request-specific state were readable inside, the first user's dashboard would be stored and served to everyone else. That is a cross-user data leak, and the framework blocks it structurally rather than trusting you to key correctly. The fix is to move the request-scoped read out to the uncached caller — a Server Component, a route handler or a Server Action — and pass only the narrow, non-identifying values the query needs down into the cached function as arguments, where they become part of the key. In practice a lot of per-user work should not be cached at all; what you cache is the shared, user-independent part, such as the plan catalogue or the team's shared project list, and you compose it with the per-user part rendered dynamically.

go deeper

for a junior

Remember that cookies() and headers() belong in uncached server code; a function marked 'use cache' takes plain arguments and must not read anything about the current request.

for a middle

Explain why the restriction exists — one entry is replayed for many requests, so request-scoped reads would be absent from the key — and show the read-outside-pass-in restructure.

for a senior

Frame it as a confidentiality failure rather than staleness, and split the page: cache the shared or tenant-scoped query behind an authorization check, stream the per-user part dynamically.

for a principal

Own the rule for the codebase — what classes of data may enter a server cache at all, who reviews it, and what invalidation on logout, permission change and deletion is required before per-user caching is allowed.

## The invariant being protected A `'use cache'` entry is computed once and replayed for every later call with the same key. That is only sound if everything capable of changing the answer is visible in the key. Request-scoped APIs are, by construction, invisible: `cookies()` and `headers()` return whatever the *current* request carried, and the cache layer has no way to fold that into a key it computed before the body ran. So if a cached function could read the session cookie, the first request would run the body, store Alice's dashboard, and every subsequent matching request would be handed Alice's dashboard — including Bob's. This is not a stale-data annoyance; it is a cross-user data leak, and it is the single most damaging failure mode in this whole area. Next therefore makes it a hard error rather than a caveat in the docs. ## The restructure The shape that works is always the same: request-scoped reads happen in uncached code, and the cached function receives plain arguments. ```ts // uncached: runs per request import { cookies } from 'next/headers' export default async function DashboardPage() { const jar = await cookies() const teamId = await resolveTeam(jar.get('session')?.value) const projects = await getTeamProjects(teamId) // cached return <ProjectList projects={projects} /> } ``` ```ts // cached: no request APIs inside export async function getTeamProjects(teamId: string) { 'use cache' return db.project.findMany({ where: { teamId } }) } ``` Now `teamId` is an argument, so it is in the key, and a team's entry can only ever be served to a request that already proved it belongs to that team. The authorization decision stays in the uncached layer where the session lives; the cache only stores the result of an already-authorized query. ## Deciding what deserves caching at all The error is a prompt to split the page, not merely to shuffle a call. Ask which part of the work is genuinely user-independent: - **Shared and stable** — the plan catalogue, navigation, published content, reference data. Cache it, generously. - **Shared within a tenant** — a team's project list, an organization's settings. Cacheable if the tenant identifier is an argument and the request has already been authorized against it. Be deliberate here: the entry now holds one tenant's data, so the key must not be guessable-into by another tenant's request path. - **Genuinely per user** — notification counts, personal drafts, anything derived from the individual's identity. Usually not worth caching: the key cardinality equals your user count, the hit rate is poor, and every entry is personal data with a retention question attached. The App Router lets you keep these on the same page: render the per-user part dynamically and stream it, while the shared part comes from cache. You are not forced to make the whole route dynamic because one widget is personal. ## Why a key alone is not the fix A tempting reply is "just add the user id to the key". Mechanically that works, but it is worth being honest about what you have built: a per-user persistent store of user data on the server, with a lifetime you set casually in a profile. That raises questions a cache normally does not — what happens on logout, on a permissions change, on an account deletion request. If the answer is "the entry keeps serving until it expires", you have a correctness and possibly a compliance problem. Tag-based invalidation on the write path is the minimum, and "do not cache this" is often the better answer. ## Recognising it in review Two smells: a cached function whose name contains a personal noun (`getMyX`, `getCurrentUserX`), and a `'use cache'` at the top of a *file* that also happens to export a session-aware helper. File-level directives are how request-scoped work sneaks into a cached module — one reason to prefer function-level placement in code that touches users.

  • Is it acceptable to fix this by passing the user id into the cached function as an argument?
    Mechanically yes — it enters the key, so entries no longer cross users. But be deliberate: you now hold per-user data in a server cache with a lifetime, so logout, permission changes and deletion all need an invalidation story, and the hit rate is poor because cardinality equals your user count. Often the honest answer is not to cache it.
  • How do you keep a page fast when part of it is per-user and part is shared?
    Split them. The shared query goes in a cached function; the per-user part stays in uncached, request-scoped code and is streamed in. The route as a whole can still serve its shared shell immediately rather than being dragged fully dynamic by one personal widget.
  • Why is a file-level `'use cache'` riskier than a function-level one in code that touches user data?
    It caches every export of the module, including ones added later by someone who did not read the top of the file. A session-aware helper dropped into that file inherits caching silently. Function-level placement keeps the decision next to the function it applies to.
  • What makes this different from an ordinary stale-data bug?
    Staleness shows old data to the person entitled to see it. This shows one user's data to another, so it is a confidentiality failure rather than a freshness one — it is not caught by a shorter lifetime, and it usually surfaces first as a support ticket rather than a metric.

saying these in an interview costs you the question

  • Thinks Next automatically adds the cookie to the cache key
  • Says it only causes stale data, not a data leak
  • Proposes a very short lifetime as the fix
  • Caches per-user data and never invalidates on logout
  • Adds 'use cache' at file level in a module with session helpers

context

open as a page

In Next.js, what does adding the `'use cache'` directive at the top of a file, an async function, or a component actually do?

level: juniorimportance: should knowfreq 48%

basics

~20 s

The 'use cache' directive marks a file, async function, or component as cacheable. Next.js runs it once, stores the returned value on the server under a key derived from its inputs, and reuses that value on later requests until it is revalidated.

open as a page

In Next.js, what does calling `cacheLife()` from `next/cache` inside a function marked `'use cache'` control, and what do the `stale`, `revalidate` and `expire` values of a cache profile mean?

level: middleimportance: should knowfreq 34%

basics

~20 s

cacheLife() sets the lifetime profile of a 'use cache' entry. Its three values separate concerns: stale is how long a client may reuse the value without re-checking, revalidate is how often the server refreshes it in the background, and expire is the maximum age past which the value can no longer be served.

open as a page

A Next.js function marked with the `'use cache'` directive takes arguments and also reads a variable from its enclosing scope. What forms the cache key, and what does that require of the arguments and of the returned value?

level: middleimportance: should knowfreq 40%

basics

~20 s

Next.js keys a 'use cache' entry on everything that flows into the call: the serialized arguments plus the values the function closes over. Both must be serializable to be part of the key, and the returned value must be serializable because it is what gets stored and replayed.

open as a page

In a Next.js server render, what does React's `cache()` function do, and how does that differ from marking the same function with Next's `'use cache'` directive?

level: middleimportance: should knowfreq 44%

basics

~20 s

React's cache() deduplicates calls within a single server render: several components asking for the same data run the work once, and the memo is discarded when the render ends. Next's 'use cache' stores the result in a persistent server cache that survives across requests and users until it is revalidated.

open as a page

Your team is upgrading a large Next.js App Router app to a version where server caching is opt-in rather than applied by default, and routes that used to be static now do work on every request. How do you decide what gets `'use cache'`, and how do you roll that out?

level: principalimportance: should knowfreq 24%

basics

~20 s

Treat it as a data-classification exercise, not a search-and-replace. Measure which routes actually regressed, cache the expensive user-independent work first with explicit lifetimes and tags, leave request-specific work uncached, and roll out route by route with staleness and hit-rate signals in place.

open as a page