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.

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.

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()).

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

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.

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

// 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:

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