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

Example · typescript
// 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.

Example · ts
// 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.

Example · ts
'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.

Example · ts
import { revalidatePath } from 'next/cache';

// Invalidate every page under /blog, including nested routes
revalidatePath('/blog', 'layout');

Discussion

  • Be the first to comment on this lesson.