revalidateTag & revalidatePath
Invalidate exactly the cached data you changed using tags and paths — the surgical alternative to time-based revalidation.
Time-based revalidate is fine for content that ages predictably, but for user-driven data you want on-demand invalidation: the moment a write happens, drop precisely the caches it affected. Two functions do this, both callable from Server Actions and Route Handlers.
Tag-based: the scalable pattern
Tag your fetches with next.tags, then call revalidateTag('tag') after a mutation. Every cached fetch carrying that tag is invalidated at once — no matter how many routes read it. This decouples 'where the data is shown' from 'when it changes', which is what makes it scale in a real app.
Path-based: the blunt instrument
revalidatePath('/blog') invalidates the Data Cache and Full Route Cache for that path, and clears the client Router Cache too. Use revalidatePath('/blog/[slug]', 'page') to hit a dynamic template, or pass 'layout' to cascade to everything nested beneath. It is simpler than tags but coarser — you throw away more than you strictly changed.
Choosing between them
- Tags — when one entity appears on many pages (a product shown on home, category, and detail). One
revalidateTag('product:42')refreshes them all. - Path — when the change maps cleanly to one route you can name.
Example
// lib/products.ts — tag the read
export async function getProduct(id: string) {
const res = await fetch(`https://api.shop.com/products/${id}`, {
next: { tags: [`product:${id}`, 'products'] },
});
return res.json();
}
// app/products/[id]/actions.ts — invalidate on write
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function updatePrice(id: string, price: number) {
await db.product.update({ where: { id }, data: { price } });
// Every page that read this product refreshes — detail, list, home
revalidateTag(`product:${id}`);
// Or, if you'd rather target the route by name:
// revalidatePath('/products/[id]', 'page');
}When to use it
- A CMS webhook calls a revalidatePath API route when an editor publishes a post, regenerating only that post's page.
- A checkout action calls revalidateTag('cart') after a purchase to clear product availability caches across all listing pages.
- A developer uses revalidatePath with layout: 'layout' to invalidate all pages under a segment without listing each URL.
More examples
On-demand revalidatePath
A POST handler secured with a secret lets a CMS call revalidatePath to regenerate a specific page on publish.
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache';
export async function POST(req: NextRequest) {
const { secret, path } = await req.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}revalidateTag in a server action
Multiple revalidateTag calls in one action invalidate both the post list and the individual post page simultaneously.
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function publishPost(id: string) {
await db.post.update({ where: { id }, data: { published: true } });
revalidateTag('posts'); // clears blog listing
revalidateTag(`post-${id}`); // clears that post's cache
}Revalidate an entire layout
Passing 'layout' as the second argument propagates invalidation down to all pages nested under that path segment.
import { revalidatePath } from 'next/cache';
// Invalidate every page under /blog, including nested routes
revalidatePath('/blog', 'layout');
Discussion