Skip to content

The Four Caching Layers

Next.js caching is four separate layers — Request Memoization, the Data Cache, the Full Route Cache, and the Client Cache (the docs’ current name for what used to be called the Router Cache) — and since v15 the Data Cache is opt-in, because fetch is no longer cached by default.

Each layer caches a different thing, lives in a different place, and is invalidated in a different way. Keeping them separate in your head is the whole skill.

LayerWhat it cachesWhere it livesLifetimeHow it is invalidated
Request MemoizationReturn value of identical fetch / cache() calls in one renderServer, per request (React)A single request / render passEnds automatically when the render finishes
Data CacheResults of data fetches, reused across requests and deploysServer (persistent)Until revalidatedrevalidateTag(tag, profile), updateTag, revalidatePath, or time-based revalidate
Full Route CacheRendered RSC payload plus HTML of a statically-rendered routeServer (persistent, build or runtime)Until a deploy or the underlying data revalidatesRevalidating the underlying data, or a redeploy
Client Cache (formerly Router Cache)RSC payloads of visited and prefetched routesClient (browser memory)Until page refresh; staleTimes controls freshnessPage refresh, router.refresh(), cookies.set / cookies.delete, or revalidateTag / revalidatePath / updateTag

Server-side: memoization, data, and route caches

Section titled “Server-side: memoization, data, and route caches”

Three of the four layers live on the server, and they stack. Request Memoization dedupes identical calls inside one render. The Data Cache persists fetch results across requests. The Full Route Cache stores the fully-rendered output of a static route so a request can skip rendering entirely.

The catch beginners miss: since Next.js 15, a plain fetch writes nothing to the Data Cache. You opt in per call.

// Not cached — hits the origin on every request (the Next.js 15+ default)
await fetch('https://api.example.com/prices')
// Opt in to the Data Cache indefinitely
await fetch('https://api.example.com/config', { cache: 'force-cache' })
// Opt in with a 60-second lifetime
await fetch('https://api.example.com/feed', { next: { revalidate: 60 } })
// Opt in AND tag for on-demand invalidation — the tag alone opts into nothing
await fetch('https://api.example.com/cart', {
next: { revalidate: 60, tags: ['cart'] },
})

next.tags by itself does not opt a fetch into the Data Cache — the bundled docs for fetch are explicit that “caching is opt-in,” and only cache: 'force-cache' or a next.revalidate number do that. Proven in the probe: a dynamic route fetching a request-counter endpoint with only next: { tags: [...] } (no cache, no revalidate) returned a fresh, incrementing count on every one of three separate requests — nothing was ever cached, so the tag had nothing to invalidate. Pair a tag with cache: 'force-cache' or next.revalidate whenever you actually want revalidateTag to do something.

The fourth layer lives in the browser (older docs and blog posts call it the Router Cache). When you navigate or hover a prefetched <Link>, Next.js keeps the route’s RSC payload in memory so back and forward are instant. Since v15 the default staleTimes.dynamic is 0, so dynamic pages are not held client-side unless you raise it.

// next.config.ts — opt into holding dynamic routes for 30s in the Client Cache (still experimental)
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
staleTimes: { dynamic: 30 },
},
}
export default nextConfig

Why “fetch is uncached by default” matters

Section titled “Why “fetch is uncached by default” matters”

In Next.js 14, fetch defaulted to force-cache, so tutorials taught “Next.js caches everything automatically.” That is wrong for v15 and v16. Today caching is layered and opt-in: a plain fetch writes nothing to the Data Cache, and the cache stays empty until you ask for it. Note what that does not mean — a plain fetch reached before any request-time API is fetched once at build and the route stays static; only cache: 'no-store' (or revalidate: 0) makes the route dynamic. GET Route Handlers changed the same way — they are dynamic unless you add export const dynamic = 'force-static' (or, under Cache Components, move the data access into a 'use cache' function).

flowchart TD
  A["Browser navigates"] --> B["Client Cache (browser)"]
  B -->|miss| C["Full Route Cache (server)"]
  C -->|miss or dynamic| D["Render Server Components"]
  D --> E["Request Memoization (per render)"]
  E --> F["Data Cache (server)"]
  F -->|miss| G["Origin: API or database"]
How a request falls through the four caches

A request falls through the four layers top to bottom, like the diagram above: Client Cache first (does the browser already have this route?), then the Full Route Cache (did the server already render this?), then, only if both miss, the render actually runs — and inside that render, Request Memoization and the Data Cache work together so the render’s own data fetching is cheap. Each layer exists to let the request skip as much of the next one as possible.

What you getBenefitCost
Data Cache opt-in by default (v15+)No stale data by surprise — every fetch is fresh unless you ask otherwiseMigrating from v14 code means every fetch that relied on default caching silently becomes a live request
next: { revalidate: N } per fetchFine-grained control per callEasy to forget on one fetch and get inconsistent freshness across a page
next: { tags: [...] }Precise on-demand invalidation laterOnly useful if you also call revalidateTag somewhere — a tag nobody revalidates is dead weight
Full Route CacheSkips rendering entirely for a static routeOnly helps routes that can be static — one request-time API anywhere in the tree opts the route out
  • “Next.js caches everything automatically.” True in Next.js 14, false since 15. A plain fetch with no options writes nothing to the Data Cache and hits the origin on every request.
  • “A plain, uncached fetch makes the route dynamic.” It does not — a fetch reached before any request-time API still runs once at build and the route stays static. Only cache: 'no-store' (or revalidate: 0) — or a genuine request-time API like cookies() — makes it dynamic.
  • “The Client Cache is the same thing as the Data Cache.” They are different layers with different lifetimes: the Data Cache lives on the server across requests, the Client Cache lives in the browser’s memory and is invalidated by router.refresh(), a page reload, or a server-triggered revalidation.
  • “Request Memoization and the Data Cache are the same layer with two names.” They are not — Request Memoization dies with the render pass; the Data Cache is what persists a fetch result across requests and deploys.

When a generated snippet fetches data and relies on caching behavior, check for these before trusting it:

  • Does a comment or assumption say “Next.js caches this automatically”? That was true in 13/14 and is false in 15/16 — verify which options are actually passed to fetch.
  • Does the code assume a plain, option-less fetch re-fetches fresh data on every request? On a route with no request-time API, it is the opposite: fetched once at build, then frozen in the static HTML until the next deploy.
  • Is next: { tags: [...] } used without cache: 'force-cache' or a next.revalidate number alongside it? Tags alone opt a fetch into nothing — there is no cache entry for revalidateTag to invalidate.
  • Are next: { tags: [...] } used anywhere without a matching revalidateTag call in the codebase? A tag nobody revalidates never gets invalidated.
  • Does the code assume an uncached fetch makes its route dynamic? It does not, by itself — only cache: 'no-store', revalidate: 0, or a request-time API does that.

🐛 Spot the bug

export default async function PricePage() {
// No cache option — Next.js re-fetches fresh data on every request, right?
const res = await fetch('https://api.example.com/live-price')
const price = await res.json()
return <p>${price.value}</p>
}
Show the answer

The opposite is true — this page has no request-time API anywhere in it (no cookies(), no headers(), no dynamic segment), so under the “auto no cache” default the fetch runs once at next build and the resulting HTML is served statically for every request afterward, frozen until the next deploy. Proven in the probe: a page like this one, pointed at a request-counter endpoint, baked a single count into the static HTML at build time, and three separate next start requests all returned that exact same frozen number — the endpoint was never hit again. A “live price” page needs an explicit cache: 'no-store' (refetch every request) or a short next: { revalidate: 5 } (a few seconds of staleness); leaving the options off is the one choice that guarantees it never updates.

Since which version is fetch uncached by default in Next.js?
Which of the four layers lives in the browser?
Which layer dedupes identical fetch calls within a single render?
What does revalidateTag invalidate?
This route has no cookies(), headers(), or dynamic segment anywhere in it. In Next.js 16, what actually happens after next build?
export default async function PricePage() {
  // No cache option — Next.js re-fetches fresh data on every request, right?
  const res = await fetch('https://api.example.com/live-price')
  const price = await res.json()
  return <p>{price.value}</p>
}
On a dynamic route (one using cookies() elsewhere), and with no cache or next.revalidate option, what does this fetch actually do on each request?
await fetch('https://api.example.com/feed', { next: { tags: ['feed'] } })