React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

revalidationrevalidatePathrevalidateTagISRcache-invalidation

Revalidation

Refresh cached data on demand or on a timer with revalidatePath, revalidateTag, and ISR.

Recipe

Quick-reference recipe card -- copy-paste ready.

// Time-based revalidation (ISR)
await fetch(url, { next: { revalidate: 60 } }); // revalidate every 60 seconds
 
// Tag-based fetching
await fetch(url, { next: { tags: ["products"] } });
 
// On-demand revalidation by tag
import { revalidateTag } from "next/cache";
revalidateTag("products");
 
// On-demand revalidation by path
import { revalidatePath } from "next/cache";
revalidatePath("/products");        // revalidate a specific page
revalidatePath("/products", "layout"); // revalidate layout and all child pages
revalidatePath("/", "layout");         // revalidate the entire app

When to reach for this: You have cached data that needs to be refreshed -- either periodically (ISR) or immediately after a mutation.

Working Example

// lib/products.ts
export async function getProducts() {
  const res = await fetch("https://api.example.com/products", {
    next: { revalidate: 300, tags: ["products"] },
  });
  if (!res.ok) throw new Error("Failed to fetch products");
  return res.json() as Promise<Product[]>;
}
 
export async function getProduct(slug: string) {
  const res = await fetch(`https://api.example.com/products/${slug}`, {
    next: { tags: ["products", `product-${slug}`] },
  });
  if (!res.ok) throw new Error("Product not found");
  return res.json() as Promise<Product>;
}
// app/actions/product.ts
"use server";
 
import { revalidateTag, revalidatePath } from "next/cache";
import { db } from "@/lib/db";
 
export async function updateProduct(formData: FormData) {
  const slug = formData.get("slug") as string;
  const name = formData.get("name") as string;
  const price = Number(formData.get("price"));
 
  await db.product.update({
    where: { slug },
    data: { name, price },
  });
 
  // Revalidate just this product's cached data
  revalidateTag(`product-${slug}`);
 
  // Also revalidate the product listing page
  revalidatePath("/products");
}
 
export async function deleteProduct(slug: string) {
  await db.product.delete({ where: { slug } });
 
  // Revalidate all product-related caches
  revalidateTag("products");
}
// app/api/revalidate/route.ts -- webhook-triggered revalidation
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
 
export async function POST(request: NextRequest) {
  const secret = request.headers.get("x-revalidation-secret");
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  const { tag } = await request.json();
  revalidateTag(tag);
 
  return NextResponse.json({ revalidated: true, tag });
}

What this demonstrates:

  • Time-based ISR with next.revalidate for background refresh
  • Granular tag-based revalidation after a mutation
  • Path-based revalidation to refresh an entire page
  • Webhook-triggered revalidation from an external CMS

Deep Dive

How It Works

  • Time-based revalidation (ISR): When next: { revalidate: N } is set, Next.js serves the cached version for N seconds. After the time expires, the next request still gets the stale version (instant response), but triggers a background regeneration. Subsequent requests get the fresh version. This is the "stale-while-revalidate" pattern.
  • On-demand revalidation with revalidateTag: Each fetch call can be tagged with next: { tags: ["my-tag"] }. Calling revalidateTag("my-tag") purges all cache entries with that tag. The next request triggers a fresh fetch.
  • On-demand revalidation with revalidatePath: Purges the full route cache for a specific path. When called with a second argument of "layout", it also revalidates all nested pages under that layout.
  • Both revalidateTag and revalidatePath can be called inside Server Actions, Route Handlers, or Middleware.
  • Revalidation does not happen during the current request -- it marks the cache as stale so the next request triggers regeneration.

Variations

Segment-level revalidation config:

// app/products/layout.tsx
// All pages under /products revalidate every 5 minutes
export const revalidate = 300;

Revalidate non-fetch data with unstable_cache:

import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";
 
const getCachedUser = unstable_cache(
  async (id: string) => db.user.findUnique({ where: { id } }),
  ["user-cache"],
  { tags: ["users"], revalidate: 600 }
);

Force dynamic rendering (opt out of all caching):

// app/dashboard/page.tsx
export const dynamic = "force-dynamic";

TypeScript Notes

// revalidatePath signature
function revalidatePath(
  originalPath: string,
  type?: "layout" | "page"
): void;
 
// revalidateTag signature
function revalidateTag(tag: string): void;
 
// Segment config export types
export const revalidate: number | false = 60;
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";

Gotchas

  • revalidatePath does not revalidate instantly -- It marks the cache as stale. The current response still serves stale data. The fresh data appears on the next request. Fix: Understand the stale-while-revalidate model; if you need instant freshness in the current response, use cache: "no-store".

  • Tags are global -- Calling revalidateTag("data") purges every cache entry tagged "data" across the entire app. Fix: Use specific, namespaced tags like "product-abc123" instead of generic ones.

  • revalidatePath("/") does not revalidate everything -- It only revalidates the root page. Fix: Use revalidatePath("/", "layout") to revalidate the root layout and all child pages.

  • ISR revalidate: 0 means no caching -- Setting revalidate to 0 opts the route into dynamic rendering, not "revalidate immediately." Fix: Use a positive integer for ISR; use cache: "no-store" for truly dynamic data.

  • Multiple revalidation times on the same route -- If different fetches in the same route use different revalidate values, Next.js uses the lowest value for the entire route. Fix: Be consistent with revalidation intervals within a route, or split into separate Suspense boundaries.

Alternatives

AlternativeUse WhenDon't Use When
cache: "no-store"Data must be fresh on every requestYou can tolerate slightly stale data
Client-side polling (SWR/React Query)You need real-time updates in the browserServer-side revalidation is sufficient
Webhook + revalidateTagExternal system (CMS, payment) triggers cache purgeYou control the mutation in your own Server Actions
revalidatePathYou want to refresh an entire page regardless of tagsYou need granular control over specific data
Full force-dynamicEvery piece of data on the route must be dynamicSome parts of the page can be static

FAQs

What is the difference between revalidatePath and revalidateTag?
  • revalidatePath purges the full route cache for a specific URL path
  • revalidateTag purges all cache entries tagged with a specific string
  • Tags offer granular, targeted invalidation; paths refresh the entire page
Does revalidatePath or revalidateTag make the current response return fresh data?
  • No. Both mark the cache as stale for the next request
  • The current response still serves stale data
  • If you need instant freshness in the current response, use cache: "no-store"
What does revalidatePath("/") actually revalidate?
  • It only revalidates the root page (/), not the entire app
  • To revalidate the root layout and all child pages, use revalidatePath("/", "layout")
What happens when different fetches in the same route use different revalidate values?
  • Next.js uses the lowest value for the entire route
  • For example, if one fetch uses revalidate: 60 and another uses revalidate: 300, the route revalidates every 60 seconds
  • Be consistent or split fetches into separate Suspense boundaries
How do you set up webhook-triggered revalidation from an external CMS?
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
 
export async function POST(request: NextRequest) {
  const secret = request.headers.get("x-revalidation-secret");
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const { tag } = await request.json();
  revalidateTag(tag);
  return NextResponse.json({ revalidated: true });
}
What does setting revalidate: 0 do?
  • It opts the route into dynamic rendering, not "revalidate immediately"
  • Effectively the same as cache: "no-store"
  • Use a positive integer for ISR behavior
How do you type the revalidatePath function signature in TypeScript?
function revalidatePath(
  originalPath: string,
  type?: "layout" | "page"
): void;
  • The second argument is optional and defaults to "page"
  • Pass "layout" to revalidate the layout and all nested pages
Can you revalidate non-fetch data sources like database queries?
  • Wrap the query with unstable_cache from next/cache and assign tags
  • Then call revalidateTag with the same tag to invalidate
import { unstable_cache } from "next/cache";
 
const getCachedUser = unstable_cache(
  async (id: string) => db.user.findUnique({ where: { id } }),
  ["user-cache"],
  { tags: ["users"], revalidate: 600 }
);
What is the segment-level revalidate config export and what does it do?
// app/products/layout.tsx
export const revalidate = 300;
  • Sets the default revalidation interval for all pages under that layout
  • Individual fetch calls can still override this value
Are tags scoped per route or are they global across the app?
  • Tags are global -- calling revalidateTag("data") purges every cache entry with that tag in the entire application
  • Use specific, namespaced tags like "product-abc123" to avoid unintended cache purges
Where can revalidateTag and revalidatePath be called from?
  • Server Actions
  • Route Handlers
  • Middleware
  • They cannot be called from Client Components or during client-side rendering
How does time-based ISR (stale-while-revalidate) work under the hood?
  • Next.js serves the cached version for N seconds after the last generation
  • After the time expires, the next request still gets the stale version instantly
  • A background regeneration is triggered, and subsequent requests receive the fresh version