Understanding Caching in Next.js 15
Next.js 15 rewrote the caching defaults that made version 14 so confusing. The four cache layers, what changed, and how to opt back in deliberately.
Caching was the most criticised part of the App Router era: Next.js 14 cached aggressively by default, and developers spent their days discovering which layer was serving them stale data. Next.js 15 inverted the philosophy — dynamic by default, cached by choice. That makes apps slower out of the box and dramatically easier to reason about, which is the right trade. But you still need to know the layers to opt back in well.
The four caches, briefly
- Request memoisation: identical fetch calls within one render are deduplicated — per request, automatic, uncontroversial
- Data Cache: stores fetch results on the server across requests; controlled per fetch with revalidate and tags
- Full Route Cache: stores the rendered HTML and payload of static routes at build or revalidation time
- Router Cache: the client-side cache that makes back and forward navigation instant
Most confusion comes from conflating the middle two: the Data Cache stores what your fetches returned; the Full Route Cache stores what your pages rendered. Invalidating data invalidates the routes built from it, but they are separate stores with separate lifetimes.
What changed in Next.js 15
Three defaults flipped, and together they explain why an upgraded app suddenly renders more dynamically:
- fetch requests are no longer cached by default — previously they were, silently
- GET Route Handlers are no longer cached by default
- The client Router Cache no longer reuses page segments by default when you navigate, so users see fresh data on every visit to a route
Nothing stops you restoring the old behaviour — the difference is that it now happens where you can see it, per fetch and per route, instead of globally and invisibly. If an upgraded app suddenly feels slower, this is why: it is doing honest work it used to skip. The fix is to reintroduce caching deliberately, not to long for the old defaults.
Opting back in
Caching decisions now live beside each request, which is exactly where a reviewer wants to find them. Tag anything you will need to invalidate later.
// cached for an hour, and invalidatable by tag
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600, tags: ['products'] },
});
// later, in a Server Action after a mutation:
import { revalidateTag } from 'next/cache';
revalidateTag('products');For whole routes, the segment-level revalidate export still works, and generateStaticParams still gives you fully static pages with the Full Route Cache doing what it always did. Tags remain the most precise tool: model them around your content — a tag per entity type, plus per-entry tags where invalidation needs to be surgical.
Looking further ahead, the experimental use cache directive signals where this is going: marking functions, components or whole files as cacheable and letting Next.js manage the keys. It is not something to build production systems on yet, but the direction of travel is clear — explicit, code-level cache declarations rather than configuration.
Debugging cache behaviour
When a page is not behaving, work through the layers in order rather than guessing. Enable fetch logging in development to see each request's cache status. Check the response headers in production — the cache status headers tell you whether a route was served static, revalidated or dynamic. And remember the order of operations: revalidateTag marks entries stale; the regeneration happens on the next request. If a stakeholder is looking at a page that will not update, the Router Cache in their own browser tab is a suspect too — a hard refresh distinguishes a server problem from a client one.
The mental model for Next.js 15 fits in a sentence: everything is dynamic unless you cache it, and everything you cache should have a plan for invalidation. Write the plan down and the whole system stays boring, in the best way.