Next.js Caching: The Four Layers, and Which One Bit You
Request memoisation, the Data Cache, the Full Route Cache and the Router Cache are four separate mechanisms with four separate lifetimes. Most caching bugs are a confusion between them.
Almost every caching complaint we are sent turns out to be the same sentence: "we changed the data and the page did not update." It is a reasonable description of the symptom and a useless description of the cause, because Next.js has four caches and the fix is different in each one.
They are worth separating, because they have different keys, different lifetimes, and different ways of being invalidated.
The four layers
| Layer | Lives | Scope | Cleared by |
|---|---|---|---|
| Request memoisation | One render pass | Server | Nothing - it expires on its own |
| Data Cache | Across requests and deploys | Server | revalidateTag, revalidatePath, time |
| Full Route Cache | Until revalidation or redeploy | Server | The same calls, plus a new build |
| Router Cache | Seconds to minutes | One visitor's browser | Navigation, router.refresh() |
Read that table twice. Three of those live on the server and one lives in the visitor's tab, and the one in the tab is the one that produces the reports that make no sense - the page is correct in an incognito window and stale for the person who filed the ticket.
Request memoisation
Inside a single render, two identical fetch calls with the same URL and
options are executed once. This is React's, not Next.js's, and it exists so
that a layout and a page can both ask for the current user without you
threading it through props.
// Both of these run; only one request leaves the server.
const user = await fetch('https://api.example.com/me').then((r) => r.json());It is scoped to one render pass. It cannot be stale, it cannot be cleared, and it is almost never the layer that caused your bug. Useful to know mainly so you stop writing caching code that duplicates it.
The Data Cache
This is the one worth understanding properly. It stores the result of fetch
on the server, and it survives requests, and it survives deploys.
That last part surprises people. A redeploy does not flush the Data Cache.
// Cached until something invalidates the tag.
const posts = await fetch('https://cms.example.com/posts', {
next: { tags: ['posts'] },
});
// Cached for at most 60 seconds.
const rates = await fetch('https://api.example.com/rates', {
next: { revalidate: 60 },
});
// Never cached.
const cart = await fetch('https://api.example.com/cart', {
cache: 'no-store',
});Tags are the mechanism that makes a content site sane. Tag the fetch, and let the CMS webhook clear it:
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return new Response('Unauthorised', { status: 401 });
}
revalidateTag('posts');
return Response.json({ revalidated: true });
}The secret check is not optional. An unauthenticated revalidation endpoint is a free cache-flush button for anyone who finds the URL, and flushing the cache on a busy site means every request goes to the origin at once.
The Full Route Cache
When a route has no dynamic inputs, Next.js renders it at build time and serves the stored HTML and RSC payload. This is the cache people mean when they say "static."
The interesting part is how easily a route leaves it. Reading cookies(),
headers() or searchParams, or calling a fetch with cache: 'no-store',
opts the whole route into dynamic rendering. One helper deep in a shared
component can do it, and the only visible signal is the build output:
Route (app)
┌ ○ / static
├ ƒ /products dynamic <- was static last week
└ ○ /about static
If a route you expected to be static shows ƒ, something under it read a
request-time value. Finding what is a matter of reading the tree, not of
guessing - and it is worth finding, because a dynamic route costs you the CDN.
The Router Cache
This one lives in the visitor's browser, and it is the reason your colleague sees old data after you told them it was fixed.
When a visitor navigates client-side, the RSC payload for the destination is kept in memory for a short window. Navigate away and back within that window and nothing is re-fetched. A hard reload clears it; a soft navigation does not.
After a mutation, clear it explicitly:
'use client';
import { useRouter } from 'next/navigation';
export function DeleteButton({ id }: { id: string }) {
const router = useRouter();
async function remove() {
await fetch(`/api/items/${id}`, { method: 'DELETE' });
router.refresh(); // Discard the client cache, re-render on the server.
}
return <button onClick={remove}>Delete</button>;
}In a Server Action, revalidatePath does both halves - it clears the server
cache and marks the client cache stale - which is why mutations belong in
actions rather than in hand-written route handlers whenever you have the
choice.
How we debug this in practice
The question is always "which layer," and it is answerable in three steps.
- Is it stale for everyone or for one person? One person, and it is the Router Cache. Everyone, and it is on the server.
- Does a redeploy fix it? If yes, it was the Full Route Cache. If no, it is the Data Cache - which, again, survives deploys.
- Does the route show
ƒin the build output? If it does and you expected○, you are not debugging a cache at all. You are debugging an accidental dynamic render, which is a different and usually more expensive problem.
Most teams we work with do not need a more aggressive caching strategy. They need to know which of the four they are currently arguing with, and a tag naming convention they actually apply.
That, and the rendering model underneath it - which is the App Router rendering model, explained properly.
