The Four Caching Layers
Request Memoization, the Data Cache, the Full Route Cache, and the Router Cache — what each one caches, its scope, and its lifetime.
Next.js caching confuses people because it is not one cache — it is four, stacked, each with a different scope and lifetime. Once you can name the layer a value lives in, 'why is my data stale?' becomes a five-second diagnosis instead of an afternoon.
The four layers
| Layer | Caches | Where | Lifetime |
|---|---|---|---|
| Request Memoization | duplicate fetch() in one render | server | single request |
| Data Cache | fetch() results across requests | server (persistent) | until revalidated |
| Full Route Cache | rendered HTML + RSC payload | server (build/persistent) | until revalidated/redeployed |
| Router Cache | RSC payload of visited routes | client (in-memory) | seconds to a session |
How a request flows through them
- Request Memoization: within one render pass, calling
fetch(sameUrl)twice hits the network once. It is React's cache, automatic forfetch, and dies when the request ends. This is why you can fetch the same data in a layout and a page without a waterfall. - Data Cache: persists
fetchresults between requests and users. Controlled bycacheandnext.revalidate. This is the layerrevalidateTagtalks to. - Full Route Cache: for static routes, the rendered output is cached so requests skip rendering entirely.
- Router Cache: on the client, visited routes' payloads are held so back/forward and re-visits are instant — and this is the one that most often makes people think 'my update didn't apply' after a mutation.
Example
// Request Memoization in action — no waterfall, one network call.
// Both the layout and the page call the same fetch; React
// dedupes it within the single server render.
async function getUser(id: string) {
// identical URL + options => memoized for this request
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { tags: [`user:${id}`], revalidate: 3600 }, // Data Cache
});
return res.json();
}
// app/users/[id]/layout.tsx
export default async function Layout({ params, children }: any) {
const { id } = await params;
const user = await getUser(id); // network hit #1
return <header>{user.name}{children}</header>;
}
// app/users/[id]/page.tsx
export default async function Page({ params }: any) {
const { id } = await params;
const user = await getUser(id); // memoized — NO second network hit
return <p>{user.email}</p>;
}When to use it
- A developer avoids double-fetching user data in a layout and page by relying on Request Memoization to dedup identical fetch calls within one render.
- A team uses the Full Route Cache to serve a pre-rendered marketing page from disk on every request without running any React code.
- A client-side Router Cache prevents a round-trip when a user navigates back to a page they visited moments ago in the same session.
More examples
Request Memoization in action
Identical fetch calls within a single server render are deduped by React; no explicit memoisation is needed.
// Both calls resolve to the same in-memory result within one render pass
const user1 = await fetch('/api/user/42').then(r => r.json()); // fetches
const user2 = await fetch('/api/user/42').then(r => r.json()); // deduped
// user1 === user2, only one HTTP request was madeData Cache with tag for invalidation
The Data Cache persists fetch results across requests; tagging them enables precise invalidation without clearing everything.
// Store in Data Cache with a tag
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// Later — bust only posts entries
import { revalidateTag } from 'next/cache';
revalidateTag('posts');Opt out of Full Route Cache
force-dynamic or revalidate=0 prevents Next.js from caching the rendered HTML in the Full Route Cache.
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
// or use:
export const revalidate = 0;
export default async function DashboardPage() {
const data = await fetchLiveData();
return <main>{data.value}</main>;
}
Discussion