---
title: "Next.js: Caching"
slug: "nextjs-caching"
category: "frontend"
tags: ["frontend", "nextjs", "caching", "performance", "revalidate"]
status: "stable"
last_updated: 2026-08-14
summary: "Next.js 16 ships Cache Components (use cache, cacheLife, cacheTag) as the stable, opt-in caching model; this page also covers the four-layer model still active on projects that have not adopted it."
related: ["[[frontend/nextjs]]", "[[frontend/nextjs-app-router]]", "[[frontend/nextjs-server-actions]]", "[[frontend/nextjs-route-handlers]]", "[[frontend/react-server-components]]", "[[ops/vercel]]", "[[seo/page-speed]]"]
---

## Overview

Next.js 16 ships Cache Components as a stable, opt-in model (`cacheComponents: true` in `next.config.ts`). It inverts the Next.js 15 default: instead of caching unless you opt out with `cache: "no-store"`, every fetch, page, and function is dynamic unless you opt in with `"use cache"`. `use cache`, `cacheLife`, and `cacheTag` are stable in 16 (the `unstable_` prefix and the experimental Partial Prerendering flag are both gone; Cache Components absorbed PPR). Projects that have not enabled `cacheComponents` keep running the four-layer Next.js 15 model described below, which still caches by default. Know which mode your project is in before reasoning about staleness.

## Mark cacheable work with `"use cache"`

With `cacheComponents` enabled, `"use cache"` is the unit of caching. Add it to the top of a function, a route file, or a layout to opt that scope into the Data Cache. Pair it with `cacheLife()` for a time-based profile and `cacheTag()` for on-demand invalidation.

```ts
async function getTopPosts() {
  "use cache";
  cacheLife("hours");
  cacheTag("posts");
  return db.post.findMany({ orderBy: { score: "desc" }, take: 10 });
}
```

Everything not marked `"use cache"` is dynamic and runs per request, which is the opposite of the Next.js 15 default. Invalidate with `updateTag("posts")` for immediate, read-your-writes consistency (the right choice inside a server action after a mutation), or `revalidateTag("posts", "hours")` for eventual consistency that serves stale content while refreshing in the background. This replaces `unstable_cache` and most uses of `revalidatePath`.

## Know the four layers (pre-Cache-Components model)

Projects that have not enabled `cacheComponents` still run the Next.js 15 four-layer model. The layers stack from innermost to outermost:

- **Request Memoization**: React `cache()` deduplicates a function call within one server render. Two components that ask for `getUser(id)` hit the source once.
- **Data Cache**: `fetch()` results and `unstable_cache()` calls persist across requests. Lives on disk (Vercel: regional). Survives deploys until invalidated.
- **Full Route Cache**: rendered HTML and RSC payload for a static route, persisted at build time and refreshed by ISR or `revalidatePath`.
- **Router Cache**: a per-session in-memory cache in the browser that holds RSC payloads for routes the user has visited. Short-lived; flushed by `revalidate`/`revalidateTag` and by navigation.

Reach for the innermost cache that answers your question. Most data lives in the Data Cache; layout chrome lives in the Full Route Cache.

## Memoize per-render fetches with React `cache`

When two server components in the same render need the same record, wrap the loader in `cache()` so the underlying call runs once.

```ts
import { cache } from "react";

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});
```

`cache` is per-render. The next request gets a fresh memo. Use it for database calls that `fetch` cannot wrap. The `fetch` API has its own dedupe within a render; no extra work needed.

## Cache `fetch` deliberately, opt out when you must

Next.js 15 does not cache `fetch` by default. Opt in with `next.revalidate` or `next.tags`. Opt out with `cache: "no-store"` (or by reading dynamic functions like `cookies()`).

```ts
// Static, refreshed every 60s.
await fetch(url, { next: { revalidate: 60, tags: ["posts"] } });

// Dynamic, never cached.
await fetch(url, { cache: "no-store" });

// Cached forever until revalidated by tag.
await fetch(url, { next: { tags: ["posts"] } });
```

`revalidate: 0` is the same as `no-store`. `revalidate: false` (or a large number) caches indefinitely. Tag every cached fetch you might invalidate later; the tag is what `revalidateTag` looks for.

## Use `unstable_cache` for non-`fetch` work (pre-Cache-Components)

Database calls, third-party SDK calls, and heavy computations do not flow through `fetch`. On a project without `cacheComponents`, wrap them with `unstable_cache` to persist results in the Data Cache. On a project with `cacheComponents` enabled, use `"use cache"` on the function instead; see above.

```ts
import { unstable_cache } from "next/cache";

export const getTopPosts = unstable_cache(
  async () => db.post.findMany({ orderBy: { score: "desc" }, take: 10 }),
  ["top-posts"],         // cache key parts
  { revalidate: 300, tags: ["posts"] }
);
```

The key array is part of the cache key; any input that changes the result must be in it. Pass the user id, the locale, the query parameters. Two callers with different keys get separate entries.

## Invalidate by tag for collections, by path for pages

`revalidateTag("posts")` invalidates every cache entry tagged `"posts"`, across every route. `revalidatePath("/posts/[slug]", "page")` invalidates the Full Route Cache and the Router Cache for the matching segment.

```ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function createPost(formData: FormData) {
  await db.post.create({ data: parse(formData) });
  revalidateTag("posts");          // any listing, any layout
  revalidatePath("/posts");        // the index page
}
```

Tag for collections (a "posts" feed shown in three places). Path for a single page that changed. Mixing is fine; document which strategy each resource uses.

## Opt out of caching with dynamic functions

Calling `cookies()`, `headers()`, `draftMode()`, or `searchParams` inside a server component opts the whole route segment out of static rendering. The Full Route Cache no longer applies; data caching still works fetch-by-fetch.

```tsx
// This page renders per-request because it reads cookies.
import { cookies } from "next/headers";
export default async function Account() {
  const session = await getSession(await cookies());
  return <Profile user={session.user} />;
}
```

Force dynamic rendering explicitly with `export const dynamic = "force-dynamic"`. Force static with `export const dynamic = "force-static"` and the build fails if the route uses dynamic functions. Pick the constraint that matches the route's data freshness.

## Control the Router Cache with `staleTimes`

The Router Cache holds RSC payloads for routes the user already visited so a back navigation paints from memory. In Next.js 15 the defaults are `dynamic: 30` and `static: 180` (seconds). The client router additionally enforces a 30s minimum stale time, so setting `dynamic: 0` does not fully disable the cache. Tune the budget per project:

```ts
// next.config.ts
export default {
  experimental: {
    staleTimes: { dynamic: 30, static: 180 },
  },
};
```

Use a higher dynamic stale time for read-heavy dashboards. For apps where the user expects fresh data after every action, rely on `revalidatePath` and `revalidateTag` from server actions rather than a low stale time; they invalidate the matching Router Cache entries directly, which a `staleTimes` value cannot guarantee given the 30s client minimum.

## Related

- [[frontend/nextjs]]
- [[frontend/nextjs-app-router]]
- [[frontend/nextjs-server-actions]]
- [[frontend/nextjs-route-handlers]]
- [[frontend/react-server-components]]
- [[ops/vercel]]
- [[seo/page-speed]]
