1. Introduction đ
Caching is one of the most powerful â and most misunderstood â parts of Next.js. Done well, it makes your app feel instant; done poorly, it serves stale data to users who expect fresh content. This tutorial breaks down every caching layer in Next.js, from request-level memoization all the way up to the CDN, and shows you how to invalidate each one intentionally.
Information
2. What is Caching? đ¤
Caching means storing the result of expensive work â a database query, a rendered page, a fetch call â so a future request can reuse it instead of redoing that work from scratch.
- Reduces redundant computation and network calls.
- Improves perceived and actual page load speed.
- Introduces a new problem: knowing when cached data is no longer valid.
3. Why Caching Matters âĄ
Without caching, every request would re-run every data fetch and re-render every component from zero â even if nothing changed since the last request a second ago. Caching trades a small risk of staleness for a large gain in speed and cost.
Tip
4. Next.js Caching System đī¸
Next.js layers four distinct caching mechanisms on top of each other, each operating at a different scope and lifetime.
| Cache | Location | Purpose | Duration |
|---|---|---|---|
| Request Memoization | Server | Dedupe identical fetch calls | Single render pass |
| Data Cache | Server | Persist fetch results | Until revalidated |
| Full Route Cache | Server | Store rendered HTML/RSC payload | Until revalidated |
| Router Cache | Client | Store visited route segments | Session-based |
5. Request Memoization đ§
Within a single render pass, calling fetch with the same URL and options multiple times only triggers one actual network request â the rest are automatically deduplicated.
app/page.tsx
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
export default async function Page() {
// Both calls resolve from the same underlying request
const userA = await getUser('1');
const userB = await getUser('1');
return <div>{userA.name}</div>;
}Note
6. Data Cache đž
The Data Cache persists the results of fetch calls across requests and deployments, unless you explicitly opt out. This is what allows a data-fetching page to remain fast without hitting the origin every time.
app/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache', // default behavior for most fetches
});
return res.json();
}7. Full Route Cache đŧī¸
At build time (or after revalidation), Next.js can cache the fully rendered RSC payload and HTML for a route â the Full Route Cache â so subsequent requests skip rendering entirely.
- Applies to statically rendered routes.
- Automatically invalidated when the underlying Data Cache entries it depends on are revalidated.
- Bypassed entirely for routes marked as dynamic.
8. Router Cache đ§
On the client, Next.js keeps an in-memory cache of previously visited route segments, making back/forward navigation and prefetched links feel instant without a server round-trip.
Information
9. Client Cache đ
Beyond the Router Cache, standard browser-level HTTP caching still applies to static assets like images, scripts, and fonts, governed by normal Cache-Control response headers.
next.config.js
module.exports = {
async headers() {
return [
{
source: '/images/:path*',
headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
},
];
},
};10. Static Caching đ§
A route is statically rendered when Next.js can determine its output at build time (or on first request) and reuse it for every subsequent visitor â the fastest possible path.
app/blog/page.tsx
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts').then((r) => r.json());
return <PostList posts={posts} />;
}11. Dynamic Caching đ
A route becomes dynamic â rendered fresh on every request â when it reads request-specific data such as cookies, headers, or search params, or when a fetch explicitly opts out of caching.
app/dashboard/page.tsx
import { cookies } from 'next/headers';
export default async function Dashboard() {
const session = cookies().get('session'); // forces dynamic rendering
return <div>Welcome back!</div>;
}12. Cache Control đī¸
You control fetch-level caching with the cache option, and time-based revalidation with next.revalidate.
lib/data.ts
// Never cache â always fetch fresh
fetch(url, { cache: 'no-store' });
// Cache indefinitely until manually revalidated
fetch(url, { cache: 'force-cache' });
// Cache, but revalidate after 60 seconds
fetch(url, { next: { revalidate: 60 } });13. Cache Tags đˇī¸
Tags let you label a cached fetch entry so it can be invalidated by name later, rather than by path â useful when one piece of data appears on multiple pages.
lib/data.ts
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: ['product', `product-${id}`] },
});
return res.json();
}14. cache()
The cache() function from React memoizes the return value of an arbitrary function for the duration of a single render â useful for deduping non-fetch work like database calls.
lib/data.ts
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});15. unstable_cache
unstable_cache extends caching to non-fetch data sources â like direct database queries â giving them the same tag- and time-based revalidation as the Data Cache.
lib/data.ts
import { unstable_cache } from 'next/cache';
export const getCachedPosts = unstable_cache(
async () => db.post.findMany(),
['posts-list'],
{ tags: ['posts'], revalidate: 3600 }
);Caution
16. revalidate âąī¸
The revalidate option â whether on a fetch call, a route segment config export, or unstable_cache â defines how long, in seconds, a cache entry stays fresh before Next.js fetches new data.
app/page.tsx
export const revalidate = 3600; // revalidate this route every hour17. Time-Based Revalidation âŗ
This is Incremental Static Regeneration in practice: a cached page keeps serving instantly, and Next.js regenerates it in the background once the revalidate window has passed.
- A request arrives for a page whose cache has expired.
- Next.js serves the stale cached version immediately.
- In the background, the page is re-rendered and the cache updated.
- The next request receives the fresh version.
18. On-Demand Revalidation đ
Rather than waiting for a timer, you can trigger revalidation immediately â typically from a webhook fired when content changes in a CMS.
app/api/webhook/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { tag } = await request.json();
revalidateTag(tag);
return Response.json({ revalidated: true });
}19. revalidatePath
revalidatePath clears the Full Route Cache and Data Cache for a specific path, forcing the next visit to that route to render fresh.
app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function publishPost(id: string) {
await db.post.update({ where: { id }, data: { published: true } });
revalidatePath('/blog');
}20. revalidateTag
revalidateTag invalidates every cached fetch entry sharing a given tag, regardless of which route it was fetched from â more precise than invalidating an entire path.
app/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function updateProduct(id: string) {
await db.product.update({ where: { id }, data: { inStock: false } });
revalidateTag(`product-${id}`);
}21. updateTag
updateTag is a newer, more surgical revalidation primitive: instead of clearing an entire tag's cache, it lets you update the cached value in place immediately after a mutation, avoiding a stale read on the very next request.
app/actions.ts
'use server';
import { updateTag } from 'next/cache';
export async function toggleLike(postId: string) {
await db.post.update({ where: { id: postId }, data: { likes: { increment: 1 } } });
updateTag(`post-${postId}`);
}Reference
22. Cache Invalidation đ§š
There's a well-known saying in computer science:
23. Dynamic Rendering Triggers đĨ
Certain APIs automatically force a route out of static rendering and into dynamic, per-request rendering. Knowing these prevents accidentally losing the Full Route Cache.
- Reading cookies() or headers().
- Accessing searchParams in a Server Component.
- A fetch call using cache: 'no-store'.
- Setting export const dynamic = 'force-dynamic'.
24. Fetch Cache Options đ§
| Option | Behavior |
|---|---|
| cache: 'force-cache' | Reuse cached data indefinitely (default for most fetches) |
| cache: 'no-store' | Never cache; always fetch fresh |
| next: { revalidate: N } | Cache, but refresh after N seconds |
| next: { tags: [...] } | Label the cache entry for tag-based invalidation |
25. Database Caching đī¸
For direct database access (no fetch involved), wrap your query functions with unstable_cache or React's cache() to get equivalent caching and tag-based invalidation.
lib/db.ts
import { unstable_cache } from 'next/cache';
export const getActiveUsers = unstable_cache(
() => db.user.findMany({ where: { active: true } }),
['active-users'],
{ tags: ['users'], revalidate: 300 }
);26. CDN Caching đ
When deployed behind a CDN, statically rendered pages and cached responses can also be cached at the edge, serving requests without ever reaching your origin server.
Information
27. Browser Caching đĨī¸
Static assets served from public/ and build output are typically sent with long-lived, immutable Cache-Control headers, since their filenames are content-hashed and change whenever the content does.
next.config.js
module.exports = {
async headers() {
return [
{ source: '/_next/static/:path*', headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }] },
];
},
};28. Performance Optimization đī¸
- Prefer tag-based revalidation over broad path revalidation for precise invalidation.
- Keep as much of your route static as possible; isolate dynamic data behind smaller, targeted components.
- Use Suspense boundaries to stream dynamic sections without blocking the whole page.
- Set realistic revalidate windows â too short defeats the purpose of caching, too long risks stale content.
29. Debugging Cache Issues đ
- Check whether the route is statically or dynamically rendered in the build output logs.
- Verify the cache and next.revalidate options on every relevant fetch call.
- Confirm that any dynamic API usage (cookies(), headers()) isn't unintentionally opting a route out of caching.
- Trace tag names end-to-end â a typo in a tag string silently breaks revalidateTag.
Tip
30. Best Practices â
- Tag data at the smallest meaningful granularity (e.g. per-item, not just per-collection).
- Prefer on-demand revalidation over very short time-based windows when data changes are event-driven.
- Keep dynamic data access scoped to small, isolated components rather than whole pages.
- Document which cache layer each revalidation call actually affects â it saves confusion later.
31. Common Mistakes đĢ
32. Frequently Asked Questions â
Yes â GET Route Handlers follow the same fetch-level caching rules; other HTTP methods are never cached by default.
revalidatePath clears everything cached for a specific route path, while revalidateTag clears every cache entry sharing a tag, regardless of which route fetched it.
By default, Next.js disables most persistent caching in development mode so you always see your latest changes reflected immediately.