Overview

Next.js 16 (current line 16.3) has two caching models. Cache Components (cacheComponents: true, introduced in 16.0) is dynamic by default, caches only what you mark with "use cache", and makes Partial Prerendering the default rendering path. Without the flag, the previous model applies and fetch is uncached unless you opt in. Check next.config.ts first. Cache Components requires the Node.js runtime and is the base for Instant Navigations.

Opt in with "use cache" and always set cacheLife

"use cache" caches the return value of an async function, component, or file. Arguments and captured variables form the cache key. Pair every directive with an explicit cacheLife; the implicit default profile is stale 5 minutes on the client, revalidates after 15 minutes on the server, and never expires by time.

import { cacheLife, cacheTag } from "next/cache";
 
async function getTopPosts() {
  "use cache";
  cacheLife("hours"); // built-ins: seconds, minutes, hours, days, weeks, max
  cacheTag("posts");
  return db.post.findMany({ orderBy: { score: "desc" }, take: 10 });
}

Arguments and return values must be serializable: no class instances, functions, or URL objects, though children and Server Actions pass through untouched.

Read request data outside cached scopes

A cached scope cannot call cookies(), headers(), or read searchParams, even through a helper. It fails with next-request-in-use-cache, sometimes only at runtime. Read the value in an uncached component under <Suspense> and pass it as an argument so it joins the cache key.

async function Profile() {
  const session = (await cookies()).get("session")?.value;
  return <CachedProfile sessionId={session} />;
}
async function CachedProfile({ sessionId }: { sessionId?: string }) {
  "use cache";
  cacheLife("minutes");
  return <div>{await fetchUserData(sessionId)}</div>;
}

Uncached fetches and runtime reads outside <Suspense> raise a blocking-route insight in dev. Call await io() from next/cache (16.3+) before Math.random(), Date.now(), or crypto.randomUUID() to keep them out of the static shell; instant = false does not silence these build errors. Use connection() only to wait for a real user request, because it also blocks prefetching. Use "use cache: private" only when the lookup is tied to one session and cannot take the runtime value as an argument; it reads cookies() directly and is not stored in a server cache across requests (the client router keeps the result for the stale time).

Know where cached entries live

The default store is per-instance memory, so on serverless, entries often do not survive between requests (build-time prerendering still works). "use cache: remote" uses a shared cache handler for a network round trip, which pays off only at a high hit rate. The cache key includes the build ID, so every deploy starts empty, remote entries included. In Draft Mode, cached scopes re-run on every request.

Invalidate with tags

Tag cached work with cacheTag, then invalidate on write.

APIWhereBehavior
updateTag("posts")Server Actions onlyExpires immediately; the next read waits for fresh data (read-your-own-writes).
revalidateTag("posts", "max")Server Actions, Route HandlersStale-while-revalidate. The second argument is required; "max" is recommended. The one-argument form is deprecated.
revalidateTag("posts", { expire: 0 })Webhooks, Route HandlersExpires immediately where updateTag is unavailable.
revalidatePath("/posts/1")Server Actions, Route HandlersInvalidates one page when tagging is overkill. A route pattern such as "/posts/[slug]" needs a "page" or "layout" second argument and invalidates every match.
refresh()Server Actions onlyRefetches uncached data on the current route; leaves the cache alone.

updateTag and revalidatePath re-render the current route in the action’s own response; revalidateTag with a profile does not. Tags are case-sensitive and limited to 256 characters.

Dedupe within one render with React cache

Wrap non-fetch loaders (ORM, SDK) in cache() from react so repeated calls in one request run once. Each "use cache" scope has its own isolated cache() scope, so pass data in as arguments. Next.js already memoizes fetch GET requests within a render.

Previous model: fetch is uncached unless you opt in

Without cacheComponents, neither fetch nor GET Route Handlers are cached by default. Opt in per request with cache: "force-cache" or next: { revalidate, tags }, and wrap non-fetch work in unstable_cache(fn, keyParts, { revalidate, tags }). Reading cookies(), headers(), draftMode(), or searchParams makes the route dynamic. Route segment config (dynamic, revalidate, fetchCache) forces static or dynamic behavior (force-static returns empty cookies() and headers() values) and errors once the flag is on.

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

The client Router Cache has experimental staleTimes with defaults of dynamic: 0 and static: 300 seconds (dynamic was 30 before Next.js 15). For use cache content the client router enforces a 30-second minimum stale time regardless of config. After a mutation, invalidate on the server rather than lowering stale times.

Migrate by replacing, not wrapping

Previous modelCache Components
export const dynamic = "force-dynamic"Remove it; everything is dynamic by default.
dynamic = "force-static" or revalidate = N"use cache" with cacheLife ("max" for static, a matching profile for N).
fetch(url, { next: { revalidate, tags } })A "use cache" function with cacheLife and cacheTag.
unstable_cache"use cache". unstable_cache and fetch entries persisted across deploys; use cache entries do not.
unstable_noStore()Remove it.
runtime = "edge", experimental_ppr, dynamicParamsRemove them; unsupported under Cache Components.
generateStaticParams returning []Return at least one param; an empty array raises empty-generate-static-params.

Enable the flag, then follow the dev-overlay insights. Set export const instant = false on a segment to defer validation for routes you have not converted.