Understanding caching
Why the page is suddenly frozen after deploying
Next.js caches in four places. Nearly every surprise comes from not knowing which one is currently at work.
This is the lesson most people get stuck on – and the reason Next.js has a reputation for being “unpredictable”. It is predictable, though, as soon as you can tell the four caches apart.
In development almost nothing is cached. Anyone investigating caching has to use npm run build && npm run start – otherwise you simply cannot see the behaviour.
The four caches
1. Request memoization during ONE page build
The same fetch twice? It still only runs once.
2. Data cache across requests and deploys
fetch responses, until you declare them stale.
3. Full route cache the finished HTML of static pages
Produced at build time, served directly afterwards.
4. Router cache in the user's browser
Recently visited pages, so "back" is instant. Like a kitchen: the ingredient delivery does not arrive fresh for every dish (Data cacheNext.js remembers `fetch` responses across requests and deploys, until you declare them stale.→). A dish that is always the same gets pre-cooked (Full route cacheThe finished HTML of a static page, produced at build time and served directly afterwards.→). And what the guest just had on the table does not need plating again (Router cacheThe store in the browser: navigating back shows the page instantly from memory.→).
The big one: static or dynamic
The largest effect comes from whether a page is produced once at build time (Static renderingThe page is produced at build time and then served identically to everyone. Fast and cheap – the default where possible.→) or fresh on every request (Dynamic renderingThe page is produced fresh on every request. Necessary as soon as it depends on cookies, headers or search params.→). Next.js decides that itself – based on what you use.
The page stays static as long as you touch none of these.
It turns dynamic as soon as you use:
• cookies()
• headers()
• searchParams
• fetch(..., { cache: "no-store" })
• export const dynamic = "force-dynamic" These Dynamic functions`cookies()`, `headers()` and `searchParams`. Use one and the page can no longer be static.→ are contagious: as soon as one appears anywhere in the tree, the whole page can no longer be static. That is why you push them as deep as possible – or wrap them in <Suspense> so only that part goes dynamic.
How long does fetched data count as fresh?
// Default: cached until you declare it stale
await fetch(url);
// Never cache – every request goes out (makes the page dynamic)
await fetch(url, { cache: "no-store" });
// At most 60 seconds old
await fetch(url, { next: { revalidate: 60 } });
// With a tag, so you can invalidate it precisely later
await fetch(url, { next: { tags: ["posts"] } }); // Applies to the whole page rather than per fetch:
export const revalidate = 3600; // rebuild hourly
export const dynamic = "force-dynamic"; // always fresh
export const dynamic = "force-static"; // always static In early versions of the App Router fetch was always cached, which surprised a lot of people. Since Next.js 15 the default is more conservative. If you are following an older guide, check its version – this is exactly the point where old and new examples contradict each other.
What you need when
- +
Static: marketing pages, blog, documentation, product catalogue
- +
revalidate: content that should be current but not to the second - +
Dynamic: anything personal – account, cart, dashboard
- −
no-storeas a reflex – that throws away the biggest advantage - −
force-dynamicon the whole page when only part of it is personal - −
Investigating caching questions in development mode
▸ In more depth: debugging caching problems optional
When a page shows the wrong data, this order helps – from the outside in:
1. Is the page static at all?
npm run build tells you:
○ static ← produced at build time
ƒ dynamic ← on every request
2. Does a hard reload (Ctrl+Shift+R) show fresh data?
→ then it was the router cache in the browser.
3. Does a new build show fresh data?
→ then it was the full route cache: you need revalidate.
4. Still stale after that?
→ it is the data cache: revalidateTag or no-store. npm run build lists every address with a symbol in front. If you see ○ where you expected ƒ, you have found the cause without reading a line of code.
Does it stick?
5 questions on this lesson. Wrong answers show up in your stats.