skip to content

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%

answer

  1. one render versus many requests
  2. dedupe now, or reuse later
  3. react's cache() dies with the request
  4. persistent means shared with strangers
  5. module-level Map is the wrong answer

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.

solid answer

~50 s

They solve different problems that both get called "caching". `cache()` from React wraps a function so that repeated calls with the same arguments during one server render return the same result — it exists so three components that each need the current user do not issue three queries. Its lifetime is the request: when that render finishes the memo is gone, so it never serves anyone else's data and never goes stale. `'use cache'` is Next's persistent layer: the result is written to a server cache keyed by the call's inputs and replayed on later requests, potentially for other users, until a lifetime expires or a tag is revalidated. The practical rule is scope-based. Use `cache()` for request-scoped deduplication, including for data you would never share — a per-user lookup is perfectly safe to memoize for one render. Use `'use cache'` for expensive, user-independent work you actively want reused across requests. They compose: a cached loader can call a `cache()`-wrapped helper.

go deeper

for a junior

Know there are two different things called caching here: React's cache() deduplicates within one server render, while Next's 'use cache' keeps a result around for later requests.

for a middle

Explain the lifetimes precisely — memo discarded at end of render versus entry persisted across requests and users — and pick the tool from the scope the data needs.

for a senior

Show the judgment: per-user work gets cache() and stays out of the persistent layer, expensive shared work gets 'use cache' with an explicit lifetime and a tag, and hand-rolled module maps get removed.

for a principal

Be ready to standardise this across a codebase — a data-access layer where deduplication is automatic and persistent caching is an explicit, reviewed decision, so individual features are not each inventing a caching strategy.

## Two caches, two lifetimes The word "cache" hides the only distinction that matters here: how long the stored value lives, and who can see it. **React's `cache()`** — imported from `react` — is a memoizer scoped to a single server render. You wrap a function; during that render, calls with matching arguments return the first call's result instead of re-running. When the render finishes, the memo is thrown away. ```ts import { cache } from 'react' import { db } from './db' export const getUser = cache(async (id: string) => { return db.user.findUnique({ where: { id } }) }) ``` **Next's `'use cache'`** is a persistent server cache. The result is written to storage keyed by the call's inputs and replayed on subsequent requests — from other users, in other processes, until it ages out or is invalidated. ## The problem `cache()` actually solves Server Components fetch where they render, which is a genuine improvement over prop-drilling data down from the top — but it means the same datum gets requested by several unrelated components on one page. A layout needs the current user for the avatar; a sidebar needs it for permissions; the page body needs it for a greeting. Without deduplication that is three identical database round-trips per render, and lifting the query to a shared parent just to pass it down reintroduces exactly the coupling Server Components removed. `cache()` lets each component call `getUser(id)` independently while the work happens once. Note what it is *not* doing: it is not deciding freshness, because the whole memo is discarded when the render ends. Every new request starts empty. That is why it is safe for personal data — nothing outlives the request that produced it. One mechanical detail worth knowing: `cache()` matches calls by argument identity, so passing a freshly-constructed object literal on each call produces distinct entries and no deduplication. Pass primitives, or a stable reference. ## When to reach for which | Need | Tool | |---|---| | Same data requested by several components in one render | `cache()` | | Expensive, user-independent work reused across requests | `'use cache'` | | Per-user data, several components, one render | `cache()` — and usually *not* `'use cache'` | | Shared data with a freshness policy and on-demand invalidation | `'use cache'` plus `cacheLife`/`cacheTag` | They are complementary rather than competing. A `'use cache'` loader may internally call a `cache()`-wrapped helper; on a cache miss, the helper still deduplicates within that one render. ## `unstable_cache`, the predecessor Before `'use cache'`, the persistent slot was filled by `unstable_cache(fn, keyParts, { revalidate, tags })` from `next/cache`. It did the same job — persist a non-`fetch` result across requests — but you supplied the key parts by hand, which made it easy to omit a closed-over value and produce a cache that returned the wrong data rather than merely stale data. Its existence is why the ecosystem accumulated three similarly-named tools; recognising it in an older codebase, and knowing it is the persistent one rather than the per-render one, is the useful part. ## The failure modes The two directions of confusion look different in production. Using `cache()` where you wanted persistence gives you a cache that never hits across requests — no bug, just no benefit, and a puzzled developer wondering why load is unchanged. Using `'use cache'` where you wanted per-render deduplication gives you a persistent entry holding data that may be request-specific, which is a leak. Worse, the same code path that would have deduplicated harmlessly now shares its result with strangers. A third mistake sits outside both: a module-level `Map` used as a hand-rolled cache. On a long-lived server that object outlives every request and is shared by all of them, with no key discipline and no eviction. It is the failure `cache()` exists to prevent, and it is still a common sight in code reviews.

  • Is React's `cache()` safe to use for per-user data?
    Yes, and it is the normal use. The memo lives only for the render triggered by that one request, so it cannot serve another user. That is precisely the difference from `'use cache'`, where the persistent entry may be replayed for anyone whose call produces the same key.
  • What was `unstable_cache` and why is it worth recognising in an older Next.js codebase?
    It was the predecessor to `'use cache'` for persisting non-fetch results: `unstable_cache(fn, keyParts, { revalidate, tags })`. Key parts were supplied manually, so omitting a closed-over value produced wrong data, not just stale data. Seeing it should prompt you to check that the key covers every input.
  • Why does wrapping with `cache()` sometimes fail to deduplicate anything?
    Calls are matched by argument identity. If each caller passes a newly-constructed object literal, no two calls match and every one re-runs. Pass primitives such as an id string, or a stable shared reference, if you want the calls to collapse into one.
  • Why is a module-level `Map` a poor substitute for either of these?
    On a long-lived server it outlives every request and is shared by all of them, so it silently becomes a cross-user cache with no keying discipline, no lifetime and no eviction. It gives you the risks of persistent caching without any of the invalidation machinery.

saying these in an interview costs you the question

  • Thinks React's cache() persists between requests
  • Calls 'use cache' 'just a better cache()'
  • Uses 'use cache' to dedupe a per-user query in one render
  • Passes a fresh object literal and expects deduplication
  • Hand-rolls a module-level Map as a request cache

context