Next.js Caching Explained: All Four Layers
Next.js has four distinct caching mechanisms. Most developers understand one or two of them. Understanding all four is what separates fast apps from truly fast apps.
Nextcraft Engineering Team
Why Caching Is Confusing in Next.js
Next.js has four separate caching layers that interact with each other. The documentation covers each one, but rarely explains how they compose. This leads to developers either caching too aggressively (stale data) or not at all (slow apps).
Let's build a mental model from the bottom up.
Layer 1: Request Memoization
Scope: Single render cycle (one request)
Location: In-memory, per-request
When you call fetch() with the same URL multiple times in a single server render, Next.js deduplicates those calls. The first call hits the network; subsequent identical calls return the cached result from memory.
// Both of these hit the network only once per request
async function UserAvatar() {
const user = await fetch('/api/me').then(r => r.json());
return <img src={user.avatar} />;
}
async function UserName() {
const user = await fetch('/api/me').then(r => r.json());
return <span>{user.name}</span>;
}
This is automatic. You get it for free with fetch. For other data sources (databases, ORMs), use React's cache() function to opt in:
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } });
});
When it clears: After the request completes. Never persists.
Layer 2: Data Cache
Scope: Persistent across requests
Location: Server-side (Vercel's data store or Node.js filesystem)
This is the fetch cache. By default, fetch in Next.js Server Components caches the response indefinitely. You control it with the cache and next.revalidate options:
// Cache forever (default)
const data = await fetch('https://api.example.com/products');
// Don't cache at all
const data = await fetch('https://api.example.com/cart', {
cache: 'no-store',
});
// Cache and revalidate every 60 seconds
const data = await fetch('https://api.example.com/prices', {
next: { revalidate: 60 },
});
// Cache with tag-based invalidation
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
Tag-based invalidation lets you purge specific cached data on demand — critical for CMS integrations and webhook-driven revalidation:
// In a webhook handler
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
revalidateTag('posts'); // Invalidates all fetches tagged 'posts'
return Response.json({ revalidated: true });
}
When it clears: On revalidation, manual revalidateTag()/revalidatePath(), or deployment.
Layer 3: Full Route Cache
Scope: Rendered HTML and RSC payload
Location: Server-side
If a route is statically renderable (no uncached dynamic data, no cookies/headers used), Next.js caches the entire rendered output — both the HTML and the React Server Component payload. Subsequent requests for that route are served from cache without re-running your components.
This is what makes static generation in the App Router so powerful. Pages that don't need per-request data get cached at build time and served in milliseconds.
// This page will be fully cached — no dynamic data
export default async function AboutPage() {
const content = await fetch('https://cms.example.com/about', {
next: { revalidate: 3600 },
});
return <AboutContent data={await content.json()} />;
}
You can opt routes out of full route caching:
export const dynamic = 'force-dynamic'; // Always re-render
export const revalidate = 0; // Same effect
When it clears: On revalidation, or when the Data Cache for any fetch used on the route is invalidated.
Layer 4: Router Cache
Scope: Navigation history
Location: Client-side, in-memory
This is the only client-side cache. When a user navigates between pages, Next.js caches the RSC payload for each route they visit. Back/forward navigation is instant — no network request needed.
The Router Cache has time-based expiry:
- Static routes: cached for 5 minutes
- Dynamic routes: cached for 30 seconds
You can't manually invalidate the Router Cache from the server. It clears on full page reload, or programmatically via router.refresh():
'use client';
import { useRouter } from 'next/navigation';
function RefreshButton() {
const router = useRouter();
return (
<button onClick={() => router.refresh()}>
Refresh
</button>
);
}
How the Layers Interact
A request flows through the caches in order:
- Router Cache — is this route cached client-side? Serve it. Done.
- Full Route Cache — is the rendered output cached server-side? Serve it. Done.
- Data Cache — for each
fetchin the route, is the response cached? Use it. - Request Memoization — deduplicate any identical fetches within this render.
Understanding this flow explains seemingly mysterious behavior:
- You call
revalidatePath()but the page still shows old data → the Router Cache on the client hasn't been cleared - Your data updates but the page doesn't → the Full Route Cache is serving stale HTML
- Two components fetch the same URL but you're being charged double API calls → you need
cache()on a non-fetch data source
A Practical Caching Strategy
For most Next.js applications:
// Static marketing pages — cache everything
export const revalidate = 3600; // Revalidate hourly
// Blog posts — cache with tag-based invalidation
const post = await fetch(`/api/posts/${slug}`, {
next: { tags: [`post-${slug}`] },
});
// User-specific data — never cache
const session = await fetch('/api/session', {
cache: 'no-store',
});
// Product prices — short TTL
const prices = await fetch('/api/prices', {
next: { revalidate: 60 },
});
The goal: cache everything that can be cached, at the most granular level possible. Serve from edge where possible. Revalidate on data change, not on a fixed timer.
Continue reading
Related articles
Why Next.js App Router Is Better for SEO Than Pages Router
The App Router isn't just a new file-system convention — it fundamentally changes how search engines crawl and index your Next.js application.
EngineeringServer Components vs Client Components: Making the Right Call
The boundary between Server and Client Components is the most consequential architectural decision you make in a Next.js application. Here's how to draw it correctly.
EngineeringBuilding High-Performance Next.js Applications for Scale
A deep dive into how we utilize App Router and React Server Components to scale our client builds effectively.
Stay Informed.
Join 1,200+ founders and engineers receiving our monthly deep dives on product engineering, design, and growth.