Next.js use cache: cache keys, lifetime and updateTag
How Next.js use cache works, in a live demo: the arguments make the cache key, cacheLife sets how long an entry lives, and updateTag clears one tag's entries.

Put "use cache" at the top of an async function and Next.js stores its result under a key built from the build, the function and its arguments: calls with the same arguments share one entry, different arguments get their own. An entry lives as long as its cacheLife (opens in a new tab) profile allows (with no call, the default profile revalidates after 15 minutes and never expires by time), and a Server Action that calls updateTag with the entry's cacheTag clears only the entries carrying that tag. We built one demo for all three on Next.js 16.4.0, and its Playwright test checks every claim this post makes about the demo. In your own app, use 16.4.1 or later: it fixes a critical RCE in use cache functions, one of the React and Next.js RCE fixes of October 2026.
How use cache works: what makes two calls share an entry
The use cache reference (opens in a new tab) lists what goes into an entry's key, serialized:
| Key input | What it means for you |
|---|---|
Build ID (or deploymentId) | A new deploy starts every entry again. |
| Function ID | A hash of the function's location and signature: the same arguments passed to another cached function are another entry. |
| Serializable arguments | Each distinct argument (or set of props) is its own entry. |
| Closure variables | Values the function reads from an outer scope are captured and keyed like arguments. |
| HMR refresh hash | Development only: an edit invalidates the cache. |
The demo has one cached function. It makes an id and a timestamp inside its body, so a new id means the body ran again:
import { cacheLife, cacheTag } from "next/cache"; export type City = "gdansk" | "krakow"; // One cached function. The entry's key is built from its argument, so getStamp("gdansk") and// getStamp("krakow") are two entries, and every call with "gdansk" shares one.export async function getStamp(city: City) { "use cache"; cacheLife("hours"); // revalidate after 1 hour, expire after 1 day cacheTag(`city-${city}`); // updateTag("city-gdansk") clears this city's entry only // Produced inside the cached body: a new id means the body ran again. return { id: crypto.randomUUID().slice(0, 8), at: new Date().toISOString() };}The page calls it three times: twice with "gdansk", once with "krakow". The two Gdansk rows show the same id and time, because the same argument returns the same entry. Krakow shows a different id: the two cities have separate entries. Nothing else differs between the calls, so the argument alone decides which entry you get.
Arguments must be serializable: primitives, plain objects, arrays, Dates, Maps and Sets work, class instances and URL objects don't, and functions or JSX only pass through (opens in a new tab) untouched. Pass the id of a record, not a client object, and the key stays small and stable.
How long a use cache entry lives
Reload the demo and the cached ids and times stay the same, while the "Not cached" row, which waits for a request with connection(), changes on every load. With JavaScript off, the browser shows the HTML the server sent first: the cached ids are in it, and the uncached row shows only its fallback. That matches the docs: with Cache Components, prerendering fills the entry (opens in a new tab) and its output becomes part of the route's static shell.
How long it stays there is the cacheLife profile. The built-in profiles (opens in a new tab), as of the docs on 19 October 2026:
| Profile | stale (browser) | revalidate (server) | expire |
|---|---|---|---|
default (no cacheLife call) | 5 minutes | 15 minutes | never |
seconds | 30 seconds | 1 second | 1 minute |
minutes | 5 minutes | 1 minute | 1 hour |
hours (the demo) | 5 minutes | 1 hour | 1 day |
days | 5 minutes | 1 day | 1 week |
weeks | 5 minutes | 1 week | 30 days |
max | 5 minutes | 30 days | 1 year |
revalidate is when the next request gets the cached copy while a fresh one is made in the background; expire is how long an entry may go unrequested before the next request waits for a fresh render. stale is for the browser: the client router keeps the output that long, with a 30-second minimum. That is the cache prefetched routes land in, the side our Next.js prefetch benchmark measured.
Short lifetimes change where an entry can go. Under the cacheLife prerendering thresholds (opens in a new tab), an expire under 5 minutes or a revalidate of 0 makes it a dynamic hole that renders at request time, so of the presets, seconds never lands in the static shell. Wrap such a component in <Suspense>.
How to clear a use cache entry with updateTag
updateTag (opens in a new tab) can only be called inside a Server Action. It expires every entry with that tag at once, and the next request waits for fresh data instead of getting the old copy. The demo's action takes the city from the button and clears that city's tag:
"use server"; import { updateTag } from "next/cache"; import type { City } from "./get-stamp"; const cities: City[] = ["gdansk", "krakow"]; // A Server Action: updateTag works only here. It expires every entry tagged with this city, so// the next render runs getStamp for that city again and reuses the other city's entry.export async function clearCity(formData: FormData) { const city = formData.get("city"); if (!cities.includes(city as City)) return; updateTag(`city-${city}`);}Press Clear on Gdansk: without a reload, Gdansk gets a new id (in both rows, since they share an entry), and Krakow keeps its id. One tag per city is what scopes the clear: updateTag expires every entry carrying the tag, so a tag shared by both cities would reset Krakow too.
The page itself is plain Server Components: the cached rows, the uncached row inside <Suspense>, and a form per city that posts to the action:
import { connection } from "next/server";import { Suspense } from "react"; import { clearCity } from "./actions";import { getStamp, type City } from "./get-stamp";import styles from "./stamps.module.css"; export default function Page() { return ( <> <div className={styles.rows}> <CachedRow city="gdansk" name="Gdansk" clearable /> <CachedRow city="gdansk" name="Gdansk again" /> <CachedRow city="krakow" name="Krakow" clearable /> <Suspense fallback={<Row name="Not cached" call="connection()" pending />}> <UncachedRow /> </Suspense> </div> <p className={styles.reload}> <a href="/demos/nextjs-use-cache">Reload</a>, then clear one city. </p> </> );} // Same function, same argument, same entry: both Gdansk rows show one id.async function CachedRow({ city, name, clearable }: { city: City; name: string; clearable?: boolean }) { const stamp = await getStamp(city); return <Row name={name} call={`getStamp("${city}")`} stamp={stamp} clear={clearable ? city : undefined} />;} // No cache: connection() waits for a request, so this runs on every load.async function UncachedRow() { await connection(); const stamp = { id: crypto.randomUUID().slice(0, 8), at: new Date().toISOString() }; return <Row name="Not cached" call="connection()" stamp={stamp} />;} type RowProps = { name: string; call: string; stamp?: { id: string; at: string }; clear?: City; pending?: boolean }; function Row({ name, call, stamp, clear, pending }: RowProps) { return ( <section className={styles.row} aria-label={name}> <div> <h2>{name}</h2> <code>{call}</code> </div> <p className={styles.stamp}> {stamp ? ( <> <output>{stamp.id}</output> <time dateTime={stamp.at}>{stamp.at.slice(11, 19)} UTC</time> </> ) : ( pending && <span>rendering…</span> )} </p> {clear && ( <form action={clearCity}> <button name="city" value={clear}> Clear </button> </form> )} </section> );}For a webhook or a Route Handler, where updateTag throws, use revalidateTag(tag, "max") (opens in a new tab): it serves the cached copy while a fresh one is made in the background.
How to add use cache to your own Next.js app
Turn on Cache Components
Set
cacheComponents: trueinnext.config.ts.use cacheis a Cache Components feature and works only with that option on.Cache a function, not a request
Put
"use cache"as the first line of an async function that loads shared data. Read cookies, headers and search params outside it and pass the values you need as arguments.Set cacheLife explicitly
Call
cacheLife("hours")(or the profile that matches how often the data changes) in every cached function, so its lifetime is visible where it is defined instead of falling back todefault.Tag each entry by what changes it
Call
cacheTag(opens in a new tab) with one tag per record and, when a list shows that record too, a second tag for the list: one entry can carry several tags.Clear the tag where the data changes
After a write in a Server Action, call
updateTagwith the record's tag. From a webhook or Route Handler, callrevalidateTag(tag, "max").Check it on a production build
Run
next buildandnext start, then reload and clear as in the demo. In development, the HMR hash is part of the key, so an edit clears the cache.
The config is one line:
const nextConfig = { cacheComponents: true };What breaks use cache
cookies() and headers() inside a cached function
A cached function can't read cookies(), headers() or searchParams, and the rule follows the call stack: a helper it calls that reads one fails the same way, with the next-request-in-use-cache (opens in a new tab) error. On a dynamically rendered route the error shows up only when the route runs, so a build can pass and next start fail. Read the value in the component and pass it in as an argument, which also puts it in the key.
Where use cache: private fits
When moving the request access out isn't practical, 'use cache: private' (opens in a new tab) allows cookies(), headers() and searchParams inside the cached scope. It is a different cache: in production, matching calls within one request reuse the result, but nothing is stored on the server across requests, and the function runs at request time, outside the static shell. The browser can keep the output for the stale time. connection() is not allowed in either directive.
Serverless hosting and new deploys
The default cache handler keeps entries in memory. On serverless, the docs say entries typically don't persist across requests (opens in a new tab), because each request can hit another instance, while build-time caching works normally. Self-hosted, one server keeps them, with cacheMaxMemorySize as the limit. If entries filled at request time matter on serverless, the docs point to 'use cache: remote' and a cache handler shared across instances. And since the build ID is in the key, every deploy starts with an empty cache.
A missing cacheLife in a nested cache
A use cache function without its own cacheLife takes the default profile, and an inner cache with a shorter lifetime can shorten it, as the nested caching rules of cacheLife (opens in a new tab) describe. Nesting a seconds cache inside an outer one that has no cacheLife fails the build during prerendering. An explicit cacheLife in every cached function avoids both.
What is the cache key for use cache?
A serialized version of the entry's inputs: the build ID (or deploymentId when you set one), a hash of the function's location and signature, and its serializable arguments or props. Variables a cached function reads from an outer scope are captured and become part of the key too. In development, an HMR refresh hash is added.
Does use cache work on serverless or Vercel?
Build-time caching works normally there, so output prerendered into the static shell is served as built. Entries filled at request time live in the default in-memory cache, which on serverless typically does not persist across requests, because each request can land on a different instance. For a cache shared across instances, the Next.js docs point to 'use cache: remote'.
What is the difference between use cache and unstable_cache?
use cache entries are keyed by the build ID, so none of them carries over to a new deploy. The Next.js docs recommend unstable_cache, for functions that are not fetch, or the fetch cache when data has to persist across deploys.
What is the difference between React cache and use cache?
React's cache memoizes a function for one server request and is invalidated for every new request. use cache stores the output under its key across requests until cacheLife or a tag ends the entry. Values stored with React cache outside a use cache function are not visible inside it, so pass data in as arguments.
How do I enable use cache in Next.js 16?
Set cacheComponents: true in next.config.ts. use cache is a Cache Components feature and works only with that option on.
