Caching Strategies in Next.js
A practical guide to Next.js caching: the data cache, full route cache, router cache, revalidation strategies, cache tags, and how to control caching behavior at every level.
What you'll learn
- ✓The four caching layers in Next.js and how they interact
- ✓Time-based and on-demand revalidation
- ✓Cache tags for granular invalidation
- ✓When and how to opt out of caching
- ✓Practical patterns for common caching scenarios
Prerequisites
- •Next.js App Router fundamentals
- •React Server Components
- •Basic understanding of HTTP caching concepts
Next.js has multiple caching layers that work together to minimize redundant work. Understanding what each layer caches, how long it caches, and how to invalidate it is essential for building apps that are both fast and up-to-date. This post covers every caching mechanism in the App Router and when to use each one.
The four caching layers
Next.js App Router has four distinct caches:
- Request Memoization — deduplicates identical fetch calls within a single render pass
- Data Cache — persists fetch results across requests and deployments on the server
- Full Route Cache — caches the rendered HTML and RSC payload for static routes
- Router Cache — caches RSC payloads on the client for visited routes
Each layer operates independently. A request flows through all of them, and each layer can serve a cached result or pass through to the next.
Request memoization
React automatically deduplicates fetch calls with the same URL and options during a single render. If three components call fetch("/api/user"), only one network request is made.
// This fetch is called in multiple components during the same render
async function getUser() {
const res = await fetch("https://api.example.com/user/1");
return res.json();
}
// app/dashboard/page.tsx
export default async function Dashboard() {
const user = await getUser(); // Fetch #1
return <DashboardContent user={user} />;
}
// app/dashboard/sidebar.tsx (rendered in the same request)
export default async function Sidebar() {
const user = await getUser(); // Deduplicated — uses result from Fetch #1
return <SidebarContent user={user} />;
}
Request memoization only lasts for the duration of a single server render. It does not persist between requests. This is a React feature, not a Next.js feature, and it only applies to fetch — not to database queries or other data fetching methods. For those, use React’s cache().
import { cache } from "react";
import { db } from "@/lib/db";
// Wrap non-fetch data sources with cache() for request deduplication
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
Data cache
The data cache stores fetch responses on the server across requests. By default, fetch results are cached indefinitely. This cache persists across deployments on hosting platforms that support it.
// Cached indefinitely by default
const res = await fetch("https://api.example.com/posts");
// Revalidate every 60 seconds (ISR)
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
// No caching — always fetch fresh data
const res = await fetch("https://api.example.com/posts", {
cache: "no-store",
});
The revalidate option implements stale-while-revalidate. After 60 seconds, the next request serves the stale data while triggering a background refetch. Subsequent requests get the fresh data.
Opting out of the data cache
There are several ways to disable caching:
// Per-fetch: use no-store
await fetch(url, { cache: "no-store" });
// Per-fetch: set revalidate to 0
await fetch(url, { next: { revalidate: 0 } });
// Per-route: export dynamic config
export const dynamic = "force-dynamic";
// Per-route: export revalidate = 0
export const revalidate = 0;
When dynamic = "force-dynamic" is set on a page or layout, all data fetching within that segment skips the cache.
Full route cache
Static routes (those that can be rendered at build time) are cached as HTML and RSC payloads. This is the equivalent of Static Site Generation (SSG). The cache is populated at build time and after revalidation.
// This page is statically cached at build time
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
A route is static when it has no dynamic functions (cookies(), headers(), searchParams), no uncached data fetching, and no dynamic = "force-dynamic" export. Next.js determines this at build time.
Routes that use dynamic functions are rendered on every request and are not stored in the full route cache.
Router cache
The router cache is a client-side cache that stores the RSC payload of visited routes. When a user navigates between pages, previously visited pages are served from this cache for instant back/forward navigation.
// When a user visits /dashboard, the RSC payload is cached on the client
// Navigating back to /dashboard serves the cached payload instantly
The router cache behavior changed in Next.js 15. Previously, pages were cached for 30 seconds (dynamic) or 5 minutes (static). Now:
- Dynamic pages are not cached by default
- Static pages are cached for 5 minutes
loading.tsxboundaries are cached for 5 minutes
You can configure the stale time:
// next.config.js
module.exports = {
experimental: {
staleTimes: {
dynamic: 30, // Cache dynamic pages for 30 seconds
static: 180, // Cache static pages for 3 minutes
},
},
};
Time-based revalidation
Time-based revalidation sets a TTL on cached data. After the time expires, the next request triggers a background revalidation.
// Per-fetch revalidation
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // Revalidate every hour
});
return res.json();
}
// Per-route revalidation (applies to all data fetching in the segment)
// app/blog/page.tsx
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPage() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
When multiple fetch calls in the same route have different revalidate values, the shortest interval is used for the entire route.
On-demand revalidation
On-demand revalidation lets you invalidate cached data programmatically, typically in response to a webhook or user action.
revalidatePath
Invalidates all cached data for a specific path.
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { path, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Invalid secret" }, { status: 401 });
}
revalidatePath(path);
return NextResponse.json({ revalidated: true });
}
// Revalidate a specific page
revalidatePath("/blog/my-post");
// Revalidate all pages under a path
revalidatePath("/blog", "page");
// Revalidate a layout (and all pages that use it)
revalidatePath("/blog", "layout");
// Revalidate everything
revalidatePath("/", "layout");
revalidateTag
Tags provide granular control over what to invalidate. Tag your fetch calls, then invalidate by tag.
// Tag your data fetches
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
return res.json();
}
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: ["posts", `post-${slug}`] },
});
return res.json();
}
async function getComments(postSlug: string) {
const res = await fetch(`https://api.example.com/posts/${postSlug}/comments`, {
next: { tags: [`comments-${postSlug}`] },
});
return res.json();
}
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
export async function POST(request: NextRequest) {
const { tag } = await request.json();
// Invalidate all posts
revalidateTag("posts");
// Or invalidate a specific post
revalidateTag("post-my-article");
// Or invalidate comments for a specific post
revalidateTag("comments-my-article");
return NextResponse.json({ revalidated: true });
}
Tags are the most precise invalidation mechanism. Use them when you want to invalidate only the data that actually changed.
Caching with server actions
Server Actions interact with caching through revalidatePath and revalidateTag.
// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";
import { db } from "@/lib/db";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
await db.post.create({
data: { title, content },
});
// Invalidate the posts cache so the list updates
revalidateTag("posts");
}
export async function updatePost(id: string, formData: FormData) {
const title = formData.get("title") as string;
await db.post.update({
where: { id },
data: { title },
});
// Invalidate both the list and the specific post
revalidateTag("posts");
revalidateTag(`post-${id}`);
}
After a server action calls revalidateTag, the next navigation or render fetches fresh data for the invalidated tags.
Caching non-fetch data
The unstable_cache function (now stable in recent versions as cache from next/cache) lets you cache the results of database queries and other non-fetch data sources.
import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";
const getCachedPosts = unstable_cache(
async () => {
return db.post.findMany({
orderBy: { createdAt: "desc" },
take: 20,
});
},
["posts-list"], // Cache key
{
revalidate: 3600, // Revalidate every hour
tags: ["posts"], // Tag for on-demand revalidation
}
);
// Usage in a server component
export default async function PostsPage() {
const posts = await getCachedPosts();
return <PostList posts={posts} />;
}
The second argument is the cache key array. It must uniquely identify this cached computation. Include any parameters that affect the result.
const getCachedPost = unstable_cache(
async (slug: string) => {
return db.post.findUnique({ where: { slug } });
},
["post-by-slug"],
{
revalidate: 3600,
tags: ["posts"],
}
);
// The slug parameter is automatically appended to the cache key
const post = await getCachedPost("my-article");
Common caching patterns
Blog with webhook revalidation
// next.config.js — static by default
// app/blog/page.tsx
export const revalidate = false; // Cache indefinitely
async function getPosts() {
const res = await fetch("https://cms.example.com/api/posts", {
next: { tags: ["posts"] },
});
return res.json();
}
// app/api/cms-webhook/route.ts
import { revalidateTag } from "next/cache";
export async function POST(request: NextRequest) {
const payload = await request.json();
if (payload.event === "post.published") {
revalidateTag("posts");
revalidateTag(`post-${payload.slug}`);
}
return NextResponse.json({ ok: true });
}
E-commerce with mixed caching
// Product pages: cached, revalidated hourly
// app/products/[id]/page.tsx
export const revalidate = 3600;
// Cart: never cached (uses cookies)
// app/cart/page.tsx
export const dynamic = "force-dynamic";
// Search results: cached for 5 minutes
// app/search/page.tsx
export const revalidate = 300;
Dashboard with real-time sections
// app/dashboard/page.tsx
export default async function Dashboard() {
// Cached: changes rarely
const plans = await fetch("/api/plans", {
next: { revalidate: 86400 },
});
// Not cached: user-specific, changes frequently
const activity = await fetch("/api/activity", {
cache: "no-store",
});
return (
<div>
<PlansSection plans={await plans.json()} />
<ActivityFeed activity={await activity.json()} />
</div>
);
}
Debugging caching behavior
Next.js provides logging options to understand caching decisions.
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
};
This logs every fetch with its caching behavior: HIT, MISS, or SKIP. During development, caching is disabled by default, so test caching behavior with next build && next start.
Understanding the four caching layers and their invalidation mechanisms is the key to building Next.js apps that are both fast and correct. Start with the defaults, add revalidation where needed, and use tags for surgical invalidation.
Related articles
- Next.js Next.js Intercepting Routes for Modals
Build modal patterns in Next.js using intercepting routes. Learn the (.) convention, combining with parallel routes, and handling hard refreshes.
- Next.js Next.js Image Optimization Strategies with next/image
Master the next/image component with advanced optimization strategies including responsive images, blur placeholders, CDN configuration, and performance tips.
- Next.js Next.js ISR and On-Demand Revalidation Explained
Master Incremental Static Regeneration and on-demand revalidation in Next.js to serve fresh content without rebuilding your entire site.
- Next.js Authentication Middleware Patterns in Next.js
Implement authentication middleware in Next.js to protect routes, redirect unauthenticated users, and manage JWT or session-based auth at the edge.