Next.js ISR and On-Demand Revalidation Explained
Master Incremental Static Regeneration and on-demand revalidation in Next.js to serve fresh content without rebuilding your entire site.
What you'll learn
- ✓How ISR serves static pages while keeping content fresh
- ✓Time-based vs on-demand revalidation strategies
- ✓Implementing revalidatePath and revalidateTag
- ✓Cache management patterns for CMS-driven sites
Prerequisites
None — this post is self-contained.
Static sites are fast but stale. Server-rendered pages are fresh but slow. Incremental Static Regeneration (ISR) gives you both: static pages that update automatically in the background. Combined with on-demand revalidation, ISR lets you serve cached content with near-instant updates when your data changes.
How ISR Works
ISR generates static pages at build time and then regenerates individual pages in the background when they are requested after a configurable time interval. The first visitor after the revalidation interval gets the stale (cached) page while a new version is generated. Subsequent visitors get the fresh page.
// app/products/[id]/page.tsx
interface Product {
id: string;
name: string;
price: number;
updatedAt: string;
}
async function getProduct(id: string): Promise<Product> {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 60 }, // Revalidate at most every 60 seconds
});
if (!res.ok) throw new Error('Failed to fetch product');
return res.json();
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p className="text-sm text-gray-500">
Last updated: {product.updatedAt}
</p>
</div>
);
}
The next: { revalidate: 60 } option tells Next.js to serve the cached version for 60 seconds. After 60 seconds, the next request triggers a background regeneration. This is called stale-while-revalidate.
Route Segment Config
You can also set revalidation at the route segment level, which applies to all fetch calls in that page:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(`https://cms.example.com/posts/${slug}`).then(
(r) => r.json()
);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
On-Demand Revalidation with revalidatePath
Time-based revalidation is simple but imprecise. If your CMS content changes, you do not want users to wait up to 60 seconds for the update. On-demand revalidation lets you invalidate specific pages immediately.
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache';
export async function POST(request: NextRequest) {
const body = await request.json();
const secret = request.headers.get('x-revalidation-secret');
// Verify the webhook secret
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { path } = body;
if (!path) {
return NextResponse.json(
{ error: 'Missing path parameter' },
{ status: 400 }
);
}
revalidatePath(path);
return NextResponse.json({
revalidated: true,
path,
timestamp: Date.now(),
});
}
Call this endpoint from your CMS webhook when content is published:
curl -X POST https://your-site.com/api/revalidate \
-H "Content-Type: application/json" \
-H "x-revalidation-secret: your-secret" \
-d '{"path": "/blog/my-updated-post"}'
Revalidating Layouts and Nested Routes
revalidatePath can target different scopes:
import { revalidatePath } from 'next/cache';
// Revalidate a specific page
revalidatePath('/blog/my-post');
// Revalidate all pages under a layout
revalidatePath('/blog', 'layout');
// Revalidate all pages matching a dynamic segment
revalidatePath('/products/[id]', 'page');
// Revalidate everything
revalidatePath('/', 'layout');
Tag-Based Revalidation with revalidateTag
Tags let you group cached data across multiple pages and invalidate them together. This is the most powerful revalidation strategy.
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: {
tags: [`product-${id}`, 'products'],
},
});
return res.json();
}
// app/products/page.tsx (product listing)
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
tags: ['products'],
},
});
return res.json();
}
Now you can invalidate selectively:
// app/api/cms-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const body = await request.json();
// Verify webhook authenticity
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.WEBHOOK_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
switch (body.event) {
case 'product.updated':
// Revalidate only the specific product and the listing
revalidateTag(`product-${body.productId}`);
revalidateTag('products');
break;
case 'product.created':
case 'product.deleted':
// Revalidate only the listing
revalidateTag('products');
break;
case 'settings.updated':
// Revalidate everything tagged with settings
revalidateTag('site-settings');
break;
}
return NextResponse.json({ revalidated: true });
}
Revalidation in Server Actions
Server Actions can trigger revalidation directly, which is useful for forms and mutations:
// app/products/[id]/edit/page.tsx
import { revalidatePath, revalidateTag } from 'next/cache';
async function updateProduct(formData: FormData) {
'use server';
const id = formData.get('id') as string;
const name = formData.get('name') as string;
const price = parseFloat(formData.get('price') as string);
await fetch(`https://api.example.com/products/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, price }),
});
// Revalidate the product page and listing
revalidateTag(`product-${id}`);
revalidatePath('/products');
}
export default function EditProduct({ product }: { product: any }) {
return (
<form action={updateProduct}>
<input type="hidden" name="id" value={product.id} />
<input name="name" defaultValue={product.name} />
<input name="price" type="number" defaultValue={product.price} />
<button type="submit">Save</button>
</form>
);
}
Combining Time-Based and On-Demand Revalidation
The best strategy for most applications uses both approaches together. Time-based revalidation acts as a safety net, while on-demand revalidation provides instant updates.
// Fetch with both a tag and a time-based revalidation
async function getPost(slug: string) {
const res = await fetch(`https://cms.example.com/posts/${slug}`, {
next: {
tags: [`post-${slug}`, 'blog'],
revalidate: 3600, // Safety net: revalidate at least every hour
},
});
return res.json();
}
If the CMS webhook fires, the page updates immediately via revalidateTag. If the webhook fails or is not configured, the page still updates within an hour via time-based revalidation.
Generating Static Pages at Build Time
Use generateStaticParams to pre-render pages at build time. New pages that are not pre-rendered will be generated on the first request and cached:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts').then((r) =>
r.json()
);
// Pre-render the 50 most recent posts at build time
return posts.slice(0, 50).map((post: any) => ({
slug: post.slug,
}));
}
// dynamicParams defaults to true, meaning non-pre-rendered
// slugs are generated on demand and cached
Set export const dynamicParams = false to return a 404 for any slug not returned by generateStaticParams.
Debugging Revalidation
To verify that revalidation is working correctly:
// Add cache status headers in next.config.js
// next.config.js
module.exports = {
headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-next-cache-tags',
value: '', // Next.js populates this automatically in dev
},
],
},
];
},
};
In development, check the terminal output for [cache: HIT] and [cache: MISS] indicators. In production on Vercel, use the x-vercel-cache response header: HIT means the response was served from cache, STALE means a revalidation was triggered, and MISS means the page was generated fresh.
You can also log revalidation events:
// app/api/revalidate/route.ts
export async function POST(request: NextRequest) {
const { path, tag } = await request.json();
console.log(`Revalidation triggered: path=${path}, tag=${tag}, time=${new Date().toISOString()}`);
if (tag) {
revalidateTag(tag);
} else if (path) {
revalidatePath(path);
}
return NextResponse.json({ revalidated: true });
}
Wrapping Up
ISR bridges the gap between static and dynamic rendering. Time-based revalidation with next: { revalidate } gives you automatic background updates with minimal configuration. On-demand revalidation with revalidatePath and revalidateTag gives you instant cache invalidation triggered by webhooks or server actions. Use tags for fine-grained control over which pages update when specific data changes. Combine both strategies for a system that is fast by default and fresh when it matters.
Related articles
- Next.js Next.js Caching Strategies Explained
Walk through the four caching layers in Next.js App Router and learn how to choose static, ISR, dynamic, or per-request fetch caching.
- Next.js Next.js Streaming and Loading UI: Instant Pages
Build instant-loading Next.js pages with streaming, Suspense boundaries, loading.tsx files, and skeleton UIs for the best user experience.
- Next.js Next.js Image Optimization Strategies with next/image
Master the next/image component with advanced optimization strategies including responsive images, blur placeholders, CDN configuration, and performance tips.
- Next.js Streaming SSR and Partial Prerendering in Next.js
Learn how Next.js streaming SSR and partial prerendering work together to deliver fast initial loads with dynamic content that streams in progressively.