Revalidation
Refresh cached data on demand or on a timer with revalidatePath, revalidateTag, and ISR.
Search across all documentation pages
Refresh cached data on demand or on a timer with revalidatePath, revalidateTag, and ISR.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
next.revalidate for background refreshnext: { 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.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.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.revalidateTag and revalidatePath can be called inside Server Actions, Route Handlers, or Middleware.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";// 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";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.
| 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 |
revalidatePath purges the full route cache for a specific URL pathrevalidateTag purges all cache entries tagged with a specific stringcache: "no-store"/), not the entire apprevalidatePath("/", "layout")revalidate: 60 and another uses revalidate: 300, the route revalidates every 60 seconds// 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 });
}cache: "no-store"function revalidatePath(
originalPath: string,
type?: "layout" | "page"
): void;"page""layout" to revalidate the layout and all nested pagesunstable_cache from next/cache and assign tagsrevalidateTag with the same tag to invalidateimport { unstable_cache } from "next/cache";
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
["user-cache"],
{ tags: ["users"], revalidate: 600 }
);// app/products/layout.tsx
export const revalidate = 300;revalidateTag("data") purges every cache entry with that tag in the entire application"product-abc123" to avoid unintended cache purgesReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥