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 appWhen 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.revalidatefor 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 forNseconds. 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: Eachfetchcall can be tagged withnext: { tags: ["my-tag"] }. CallingrevalidateTag("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
revalidateTagandrevalidatePathcan 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
-
revalidatePathdoes 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, usecache: "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: UserevalidatePath("/", "layout")to revalidate the root layout and all child pages. -
ISR
revalidate: 0means no caching -- Setting revalidate to0opts the route into dynamic rendering, not "revalidate immediately." Fix: Use a positive integer for ISR; usecache: "no-store"for truly dynamic data. -
Multiple revalidation times on the same route -- If different fetches in the same route use different
revalidatevalues, 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
| Alternative | Use When | Don't Use When |
|---|---|---|
cache: "no-store" | Data must be fresh on every request | You can tolerate slightly stale data |
| Client-side polling (SWR/React Query) | You need real-time updates in the browser | Server-side revalidation is sufficient |
Webhook + revalidateTag | External system (CMS, payment) triggers cache purge | You control the mutation in your own Server Actions |
revalidatePath | You want to refresh an entire page regardless of tags | You need granular control over specific data |
Full force-dynamic | Every piece of data on the route must be dynamic | Some parts of the page can be static |
FAQs
What is the difference between revalidatePath and revalidateTag?
revalidatePathpurges the full route cache for a specific URL pathrevalidateTagpurges 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: 60and another usesrevalidate: 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_cachefromnext/cacheand assign tags - Then call
revalidateTagwith 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
Related
- Fetching -- Setting up fetch with cache options
- Server Actions -- Calling revalidation after mutations
- Caching -- Understanding the data cache layer
- Static vs Dynamic Rendering -- How revalidation affects rendering mode