Next.js Caching Strategies Explained
Next.js Caching Strategies Explained
Next.js has one of the most sophisticated caching systems of any web framework. Understanding it properly means faster pages, lower server costs, and knowing exactly when users see fresh data. Getting it wrong means stale data, confusing bugs, and unnecessary re-fetching.
The Four Cache Layers
Next.js has four distinct caches that operate independently:
1. Request Memoization
What it is: Within a single server render, identical fetch() calls with the same URL and options are deduplicated.
When it helps: When multiple Server Components at different levels need the same data.
// Without memoization: 3 separate HTTP requests
async function Header() {
const user = await fetchCurrentUser(); // HTTP request 1
return <UserAvatar user={user} />;
}
async function Sidebar() {
const user = await fetchCurrentUser(); // HTTP request 2
return <UserMenu user={user} />;
}
async function Page() {
const user = await fetchCurrentUser(); // HTTP request 3
return <PageContent user={user} />;
}
// With memoization (default behavior): 1 HTTP request, 3 cache hits
// All three components call the same URL — only one request goes out
Scope: Per request only. Not shared between different users or requests.
Reset: Automatic — never think about it.
2. Data Cache
What it is: Cached fetch() responses stored on disk, shared across multiple requests.
Default: Indefinite cache (static behavior — like SSG).
// Indefinite cache — fetched once, served forever (until redeployment or manual invalidation)
const data = await fetch("https://api.example.com/products");
// Cache for 60 seconds — fresh every minute
const data = await fetch("https://api.example.com/prices", {
next: { revalidate: 60 },
});
// No cache — fresh on every request (like SSR)
const data = await fetch("https://api.example.com/live-scores", {
cache: "no-store",
});
// Tagged cache — invalidate on demand
const data = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
Scope: Across all requests and deployments (until invalidated).
Reset: Manual via revalidateTag() / revalidatePath(), or TTL via revalidate.
3. Full Route Cache
What it is: The complete rendered HTML of a static page, stored on the server and served without running any React code.
When it applies: Routes with no dynamic data (all fetch calls use the default indefinite cache, no cache: "no-store", no cookies(), no headers()).
// This page will be fully cached at build time
// Every visitor gets the same pre-rendered HTML instantly
export default async function BlogPage() {
const posts = await fetch("https://api.example.com/posts").then(r => r.json());
return <PostList posts={posts} />;
}
Scope: Entire route, server-side.
Reset: Redeployment, revalidatePath(), or revalidateTag().
4. Router Cache
What it is: Browser-side cache of page data the user has already visited. Navigation to cached routes is instant.
How long it lasts:
- Static routes: 5 minutes
- Dynamic routes: 30 seconds
When it helps: Going back to a page you already visited. Navigation between tabs in a dashboard.
Reset: router.refresh(), form submission, Server Action call.
Choosing the Right Cache Strategy
Content never changes (legal pages, FAQs):
→ Default (indefinite cache)
→ Add revalidate if content might change after deployment
Content changes on a schedule (blog posts, product catalog):
→ next: { revalidate: 300 } (every 5 minutes)
→ Or: revalidateTag when content is updated
Content changes when someone takes action (CMS publish, admin update):
→ next: { tags: ["posts"] }
→ Call revalidateTag("posts") when a post is published
Content is user-specific (dashboard, profile, orders):
→ cache: "no-store"
→ Or: read from cookies()/headers() — this automatically opts out
Content is truly real-time (live prices, notifications):
→ cache: "no-store"
→ Consider WebSockets or SSE on the client side
On-Demand Revalidation
When content changes (a CMS publishes a post, an admin edits a product), invalidate the cache immediately:
// src/app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const secret = request.headers.get("x-revalidate-secret");
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { tag, path } = await request.json();
if (tag) {
revalidateTag(tag); // invalidate all fetches tagged with this
}
if (path) {
revalidatePath(path); // invalidate a specific URL path
}
return NextResponse.json({ revalidated: true, timestamp: Date.now() });
}
Call this from your CMS webhook when content is published. The next user to visit gets fresh data.
The Opt-Out Pattern
Any of these automatically make a route dynamic (no Full Route Cache):
// Reading cookies or headers — opt-out
import { cookies, headers } from "next/headers";
export default async function Page() {
const cookieStore = cookies(); // dynamic!
const userAgent = headers().get("user-agent"); // dynamic!
}
// fetch with no-store — opt-out
const data = await fetch(url, { cache: "no-store" });
// Using dynamic functions
export const dynamic = "force-dynamic"; // explicit opt-out
// Dynamic route params that aren't pre-generated
// /products/[id] with no generateStaticParams — dynamic
Explicit Route Segment Config
Override caching behavior at the route level:
// Force this route to be fully static
export const dynamic = "force-static";
// Force this route to always re-render on every request
export const dynamic = "force-dynamic";
// Cache the route, revalidate every hour
export const revalidate = 3600;
Debugging Cache Issues
When data seems stale or you're not sure what's cached:
# See what Next.js generated at build time
npm run build
# The build output shows which routes are static (●), dynamic (ƒ), or ISR (◐)
# Force a fresh fetch during development
# In next.config.ts:
experimental: {
staleTimes: {
dynamic: 0, # Don't cache dynamic routes in development
}
}
In development, the Data Cache is disabled by default. You always get fresh data. The caching behavior only kicks in when you run npm run build.
Next lesson: SWR and React Query — smart client-side data fetching with automatic caching and revalidation.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises