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.
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
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringNext.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources