The Four Caching Layers
The idea in one sentence
Section titled “The idea in one sentence”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.
Four layers, four jobs
Section titled “Four layers, four jobs”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.
| Layer | What it caches | Where it lives | Lifetime | How it is invalidated |
|---|---|---|---|---|
| Request Memoization | Return value of identical fetch / cache() calls in one render | Server, per request (React) | A single request / render pass | Ends automatically when the render finishes |
| Data Cache | Results of data fetches, reused across requests and deploys | Server (persistent) | Until revalidated | revalidateTag(tag, profile), updateTag, revalidatePath, or time-based revalidate |
| Full Route Cache | Rendered RSC payload plus HTML of a statically-rendered route | Server (persistent, build or runtime) | Until a deploy or the underlying data revalidates | Revalidating the underlying data, or a redeploy |
| Client Cache (formerly Router Cache) | RSC payloads of visited and prefetched routes | Client (browser memory) | Until page refresh; staleTimes controls freshness | Page 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 indefinitelyawait fetch('https://api.example.com/config', { cache: 'force-cache' })
// Opt in with a 60-second lifetimeawait fetch('https://api.example.com/feed', { next: { revalidate: 60 } })
// Opt in AND tag for on-demand invalidation — the tag alone opts into nothingawait 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.
Client-side: the Client Cache
Section titled “Client-side: the Client Cache”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 nextConfigWhy “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"]
Mental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Data Cache opt-in by default (v15+) | No stale data by surprise — every fetch is fresh unless you ask otherwise | Migrating from v14 code means every fetch that relied on default caching silently becomes a live request |
next: { revalidate: N } per fetch | Fine-grained control per call | Easy to forget on one fetch and get inconsistent freshness across a page |
next: { tags: [...] } | Precise on-demand invalidation later | Only useful if you also call revalidateTag somewhere — a tag nobody revalidates is dead weight |
| Full Route Cache | Skips rendering entirely for a static route | Only helps routes that can be static — one request-time API anywhere in the tree opts the route out |
Common Misconceptions
Section titled “Common Misconceptions”- “Next.js caches everything automatically.” True in Next.js 14, false since 15. A plain
fetchwith 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'(orrevalidate: 0) — or a genuine request-time API likecookies()— 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.
Reading AI-generated code
Section titled “Reading AI-generated code”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
fetchre-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 withoutcache: 'force-cache'or anext.revalidatenumber alongside it? Tags alone opt a fetch into nothing — there is no cache entry forrevalidateTagto invalidate. - Are
next: { tags: [...] }used anywhere without a matchingrevalidateTagcall in the codebase? A tag nobody revalidates never gets invalidated. - Does the code assume an uncached
fetchmakes its route dynamic? It does not, by itself — onlycache: '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.