---
title: "Next.js use cache: cache keys, lifetime and updateTag"
description: "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."
canonical_url: "https://www.outofplace.space/blog/nextjs-use-cache"
language: "en"
last_updated: "2026-10-19"
alternates:
  pl: "https://www.outofplace.space/pl/blog/use-cache-w-nextjs-jak-dziala.md"
---

# 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.

Published 2026-10-19 · Category: Next.js & React · Tags: Next.js, Caching, React

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`](https://nextjs.org/docs/app/api-reference/functions/cacheLife) 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](https://www.outofplace.space/blog/dev-digest-2026-10-18).

Live demo: https://lab.outofplace.space/demos/nextjs-use-cache (Next.js 16.4.0; source at commit 6ad61f5: https://github.com/outof-place/outofplace-lab/tree/6ad61f5c5a9cbe5835d658cf51f001e1126e1d82/app/demos/nextjs-use-cache)

## How use cache works: what makes two calls share an entry

The [`use cache` reference](https://nextjs.org/docs/app/api-reference/directives/use-cache#cache-keys) 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:

app/demos/nextjs-use-cache/get-stamp.ts (https://github.com/outof-place/outofplace-lab/blob/6ad61f5c5a9cbe5835d658cf51f001e1126e1d82/app/demos/nextjs-use-cache/get-stamp.ts)

```ts
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](https://nextjs.org/docs/app/api-reference/directives/use-cache#pass-through-non-serializable-arguments) 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](https://nextjs.org/docs/app/api-reference/directives/use-cache#cache-output) and its output becomes part of the route's static shell.

How long it stays there is the `cacheLife` profile. The [built-in profiles](https://nextjs.org/docs/app/api-reference/functions/cacheLife#preset-cache-profiles), 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](https://www.outofplace.space/blog/nextjs-prefetch-benchmark) measured.

Short lifetimes change where an entry can go. Under the [cacheLife prerendering thresholds](https://nextjs.org/docs/app/api-reference/functions/cacheLife#prerendering-behavior), 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`](https://nextjs.org/docs/app/api-reference/functions/updateTag) 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:

app/demos/nextjs-use-cache/actions.ts (https://github.com/outof-place/outofplace-lab/blob/6ad61f5c5a9cbe5835d658cf51f001e1126e1d82/app/demos/nextjs-use-cache/actions.ts)

```ts
"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:

app/demos/nextjs-use-cache/page.tsx (https://github.com/outof-place/outofplace-lab/blob/6ad61f5c5a9cbe5835d658cf51f001e1126e1d82/app/demos/nextjs-use-cache/page.tsx)

```tsx
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")`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag): 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

**1. Turn on Cache Components.**
Set `cacheComponents: true` in `next.config.ts`. `use cache` is a Cache Components feature and works only with that option on.

**2. 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.

**3. 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 to `default`.

**4. Tag each entry by what changes it.**
Call [`cacheTag`](https://nextjs.org/docs/app/api-reference/functions/cacheTag) 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.

**5. Clear the tag where the data changes.**
After a write in a Server Action, call `updateTag` with the record's tag. From a webhook or Route Handler, call `revalidateTag(tag, "max")`.

**6. Check it on a production build.**
Run `next build` and `next 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:

```ts title="next.config.ts"
const nextConfig = { cacheComponents: true };
```

## What breaks use cache

[GHSA-673q-mjm6-r5xr](https://github.com/vercel/next.js/security/advisories/GHSA-673q-mjm6-r5xr), published on 14 October 2026, is a critical remote code execution in `use cache` functions that return multiple typed arrays, on Next.js 16.0.0 to 16.4.0. There is no workaround, and the advisory recommends upgrading to 16.4.1 even if no function of yours returns typed arrays.

### 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`](https://nextjs.org/docs/app/api-reference/directives/use-cache#request-time-apis) 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'`](https://nextjs.org/docs/app/api-reference/directives/use-cache-private) 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](https://nextjs.org/docs/app/api-reference/directives/use-cache#runtime-caching-considerations), 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](https://nextjs.org/docs/app/api-reference/functions/cacheLife#nested-caching-behavior) 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.

## Further reading

- [Next.js: use cache](https://nextjs.org/docs/app/api-reference/directives/use-cache): Cache keys, serialization, runtime caching per host and revalidation.

- [Next.js: cacheLife](https://nextjs.org/docs/app/api-reference/functions/cacheLife): The preset profiles with their stale, revalidate and expire times, and the prerendering thresholds.

- [Next.js: updateTag](https://nextjs.org/docs/app/api-reference/functions/updateTag): Clearing a tag from a Server Action, and how it differs from revalidateTag.

- [Next.js: use cache: private](https://nextjs.org/docs/app/api-reference/directives/use-cache-private): Caching functions that read cookies or headers, and what it does not store.

- [Next.js: Remote Code Execution in use cache functions](https://github.com/vercel/next.js/security/advisories/GHSA-673q-mjm6-r5xr): The advisory of 14 October 2026: affected versions 16.0.0 to 16.4.0, fixed in 16.4.1.

- [Next.js prefetch, measured: Partial Prefetching in 16.3](https://www.outofplace.space/blog/nextjs-prefetch-benchmark): Partial Prefetching in Next.js 16.3 cut prefetched bytes by up to 93% in our benchmark, but URL content now lands after the click. When each setup wins.
