Next.js Caching Explained: The Four Layers and How to Control Them
The Request Memoization, Data Cache, Full Route Cache and Router Cache — what each one caches, how long for, and how to opt out correctly.
Table of contents
- The four layers
- Request Memoization
- The Data Cache
- Invalidating on demand
- The Full Route Cache
- The client Router Cache
- A debugging order
- Frequently asked questions
- Why is my data stale after a mutation?
- Does cache: 'no-store' make the whole route dynamic?
- How do I cache a third-party SDK call?
- Is ISR still a thing?
- Related reading
- References
Next.js caching confuses people because there are four independent caches with different lifetimes, and "why is my data stale?" has four possible answers.
The four layers#
| Cache | Scope | Lifetime | Location |
|---|---|---|---|
| Request Memoization | one render pass | that request | server |
| Data Cache | fetch results | until revalidated | server, persistent |
| Full Route Cache | rendered HTML/RSC | until revalidated | server, persistent |
| Router Cache | RSC payload | session, ~30s | browser |
Request Memoization#
Within a single render, identical fetch calls are deduplicated automatically. This is what lets generateMetadata and the page component both call getUser(id) without two round-trips — no cache configuration, no prop drilling.
It only applies to fetch. A direct database call is not deduplicated; wrap it in React's cache() for the same effect:
import { cache } from 'react';
export const getUser = cache(async (id: string) => db.users.find(id));The Data Cache#
In Next 15, fetch is no longer cached by default — a significant change from 14. Opt in explicitly:
// Cached indefinitely until revalidated
await fetch(url, { cache: 'force-cache' });
// Cached for 60 seconds (ISR)
await fetch(url, { next: { revalidate: 60 } });
// Never cached (the Next 15 default)
await fetch(url, { cache: 'no-store' });
// Tagged, for targeted invalidation
await fetch(url, { next: { tags: ['posts'] } });For non-fetch data sources, unstable_cache gives you the same controls:
const getPosts = unstable_cache(async () => db.posts.findMany(), ['posts'], {
revalidate: 3600,
tags: ['posts'],
});Invalidating on demand#
Tag-based revalidation is the feature that makes ISR practical:
'use server';
export async function publishPost(data: FormData) {
await db.posts.create(/* ... */);
revalidateTag('posts'); // every cache entry tagged 'posts'
revalidatePath('/blog'); // and this specific route
}This is far better than a short revalidate window: content appears immediately after a write instead of up to N seconds later, and you serve cached responses the rest of the time.
The Full Route Cache#
A statically-rendered route's output is cached at build time. A route becomes dynamic — and therefore uncached — the moment it uses cookies(), headers(), searchParams, or an uncached fetch.
The build output tells you which you got:
○ (Static) prerendered as static content
● (SSG) prerendered using generateStaticParams
ƒ (Dynamic) server-rendered on demandIf a page you expected to be static shows ƒ, something in its tree read a dynamic input. That is worth chasing down — a static page is served from the CDN with no compute at all.
The client Router Cache#
Navigations store the RSC payload in memory for ~30 seconds. This is why clicking back to a page can show stale data even after a server revalidation. router.refresh() clears it, and Server Actions clear it automatically for the paths they revalidate.
A debugging order#
- Check the build output — is the route
○,●orƒ? - Check the
fetchoptions at the data source. - Check whether a Server Action revalidated the right tag or path.
- Only then suspect the client Router Cache.
Frequently asked questions#
Why is my data stale after a mutation?#
Almost always a missing revalidateTag/revalidatePath in the Server Action. Writing to the database does not invalidate anything by itself.
Does cache: 'no-store' make the whole route dynamic?#
Yes. An uncached fetch opts the route out of the Full Route Cache.
How do I cache a third-party SDK call?#
unstable_cache, since it is not a fetch. Give it a key array and tags so you can invalidate it.
Is ISR still a thing?#
Yes — next: { revalidate: n } is ISR. The tag-based API is a more precise version of the same idea.