Skip to main content

Incremental Static Regeneration (ISR): Advanced Caching

Incremental Static Regeneration (ISR) is a caching pattern where Next.js pre-builds pages at deploy time (fast static delivery) but automatically regenerates them on a schedule (fresh data). When a page's revalidation time expires, the next request triggers a background regeneration while serving the stale page to users—so nobody waits. ISR combines the performance of static sites (sub-50 ms response times) with the freshness of dynamic sites (data updates automatically). For content that changes occasionally (blogs, product catalogs), ISR is optimal.

How ISR Works

ISR operates on a simple timeline. Suppose you have a blog post with revalidate: 3600 (one hour):

  1. At deploy time: Next.js builds the page and caches it.
  2. 0–60 min after deploy: Requests serve the cached page instantly (<50 ms).
  3. At 60 min: The next request arrives. Next.js detects the cache is stale, immediately serves the cached version to the user, and kicks off a background regeneration in the server.
  4. Regeneration completes (1–5 seconds later): The cache is updated. The next request gets fresh data.
  5. 60–120 min: Requests serve the fresh cached page.
  6. Cycle repeats.

The key insight: the user never waits for regeneration. They get the cached version (stale but fast), and the page updates in the background. This is dramatically better than dynamic rendering, where every request must wait for page computation.

According to Next.js benchmarks (2026), ISR reduces Time to First Byte by 50–70% compared to fully dynamic rendering while keeping data fresh within hours.

Implementing Time-Based ISR

Use the revalidate export (page-level) or the next: { revalidate } fetch option (per-fetch):

// app/blog/[slug]/page.js
export const revalidate = 3600; // Regenerate every hour

export default async function BlogPostPage({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 }
}).then(r => r.json());

return (
<article>
<h1>{post.title}</h1>
<p className="date">{new Date(post.publishedAt).toDateString()}</p>
<p>{post.content}</p>
</article>
);
}

The export const revalidate = 3600 line tells Next.js to regenerate this page every hour. The next: { revalidate: 3600 } in the fetch controls that specific fetch's cache.

For dynamic segments (like [slug]), specify which pages to pre-build at deploy time using generateStaticParams:

// app/blog/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
}).then(r => r.json());

// Return the top 50 posts; others are built on-demand
return posts.slice(0, 50).map(post => ({
slug: post.slug
}));
}

export const revalidate = 3600;

export default async function BlogPostPage({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 }
}).then(r => r.json());

return <article>{/* ... */}</article>;
}

At build time, Next.js pre-builds the top 50 blog posts. When users visit a post not in the top 50, it's built on-demand and cached for subsequent requests (called "On-Demand ISR"). This prevents extremely long build times for sites with millions of pages.

On-Demand Revalidation

ISR's time-based approach regenerates on a schedule, but sometimes you need immediate updates—like when an editor publishes a post. Use on-demand revalidation with revalidatePath() or revalidateTag() from a server action or API route:

// app/actions/publish.js (server action)
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function publishPost(postData) {
// Save to database
await db.posts.create(postData);

// Immediately revalidate the blog listing and this post
revalidatePath('/blog');
revalidateTag('blog-posts');

return { success: true };
}

When a post is published, revalidatePath('/blog') regenerates the blog listing page, and revalidateTag('blog-posts') regenerates all pages tagged with 'blog-posts'. This happens within seconds, so users see new content immediately without waiting for the scheduled revalidation.

Call this action from a form submission in your CMS or admin panel:

// app/admin/publish-form.js
'use client';

import { publishPost } from '@/app/actions/publish';

export function PublishForm() {
async function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const result = await publishPost({
title: formData.get('title'),
slug: formData.get('slug'),
content: formData.get('content')
});
if (result.success) {
alert('Published and cache revalidated!');
}
}

return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Post title" required />
<input name="slug" placeholder="URL slug" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Publish</button>
</form>
);
}

This pattern is ideal for headless CMS integrations (Contentful, Sanity, etc.). When content updates in the CMS, a webhook calls your API route, which calls revalidatePath(), and your Next.js site updates instantly.

Tag-Based Caching for Fine-Grained Control

Tags enable revalidation of multiple related pages without affecting others. For example, if a product changes, revalidate only pages related to that product:

// app/products/[id]/page.js
export const revalidate = 86400; // Regenerate daily

export default async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { tags: [`product-${params.id}`, 'products'] }
}).then(r => r.json());

return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}

// app/products/page.js (listing)
export const revalidate = 3600; // Regenerate hourly

export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }
}).then(r => r.json());

return (
<div>
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
);
}

// app/admin/revalidate-product.js (API route)
export async function POST(req) {
const { productId } = await req.json();

// Revalidate only this product and the product listing
revalidateTag(`product-${productId}`);
revalidateTag('products');

return { revalidated: true };
}

When a product is updated, only pages with that product's tag regenerate. The rest of the site remains unchanged. This is more efficient than revalidatePath('/products/*'), which regenerates all product pages.

Real-World Example: E-Commerce Catalog

Here's a complete ISR setup for an e-commerce site:

// app/products/[id]/page.js
export const revalidate = 86400; // Daily refresh

export async function generateStaticParams() {
// Pre-build top 100 products; rest built on-demand
const products = await fetch('https://api.example.com/products?limit=100', {
next: { revalidate: 3600 }
}).then(r => r.json());

return products.map(p => ({ id: p.id.toString() }));
}

export default async function ProductPage({ params }) {
const product = await fetch(
`https://api.example.com/products/${params.id}`,
{ next: { tags: [`product-${params.id}`] } }
).then(r => r.json());

const reviews = await fetch(
`https://api.example.com/products/${params.id}/reviews`,
{ next: { tags: [`reviews-${params.id}`], revalidate: 300 } } // Reviews refresh more often
).then(r => r.json());

return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<div className="reviews">
{reviews.map(r => <div key={r.id}>{r.text}</div>)}
</div>
</div>
);
}

// app/admin/api/update-price/route.js (API route for webhook)
import { revalidateTag } from 'next/cache';

export async function POST(req) {
const { productId, newPrice } = await req.json();

// Update database
await db.products.update(productId, { price: newPrice });

// Revalidate immediately
revalidateTag(`product-${productId}`);

return { success: true };
}

Product pages regenerate daily automatically. When an admin updates a price via a webhook, that specific product page is revalidated immediately. Reviews, which change more frequently, revalidate every 5 minutes (faster than the product itself).

ISR Performance Gains

ISR significantly reduces infrastructure costs and improves performance compared to fully dynamic rendering. A typical benchmark (Vercel, 2025):

  • Fully static: <50 ms TTFB, but stale until deployment
  • ISR (hourly revalidate): <50 ms TTFB (cached), automatically fresh every hour
  • Fully dynamic: 200–400 ms TTFB (compute per-request), always fresh
  • Cost (monthly, 1M requests):
    • Static: $50 (CDN only)
    • ISR: $60 (CDN + occasional server compute)
    • Dynamic: $200+ (server for every request)

ISR is the "Goldilocks" solution for most content sites: fast, fresh, and affordable.

Key Takeaways

  • ISR pre-builds pages (fast static delivery) and regenerates them on a schedule (fresh data).
  • Use export const revalidate = <seconds> to set regeneration frequency per page.
  • On-demand revalidation via revalidatePath() or revalidateTag() updates pages immediately when content changes.
  • Tag-based caching allows fine-grained invalidation of related pages without affecting others.
  • ISR is ideal for blogs, product catalogs, and any content that changes occasionally but not per-request.

Frequently Asked Questions

Does ISR regeneration block user requests?

No. Regeneration happens in the background. Users receive the stale cached page immediately while the server regenerates in the background. Once regeneration completes, the next request gets fresh data.

What if regeneration fails?

The cached version remains in use. Next.js will retry regeneration on the next request. If persistent failures occur, log the error and investigate your data source (API or database).

Can I use ISR with dynamic routes like [id]?

Yes, with generateStaticParams(). This function pre-builds specific routes (e.g., top 100 products). Other routes are built on-demand and cached. Check Vercel docs for On-Demand ISR, which builds pages lazily as users visit them.

How do I monitor revalidation in production?

Use Vercel Analytics or server logs. Check logs for revalidation events; they appear as page rebuilds. You can also set up alerts for regeneration failures via Vercel Observability or external monitoring tools.

Does ISR work with static exports (next export)?

No. ISR requires a server (Vercel, AWS Lambda, etc.) to perform background regeneration. Static exports build once and never update. Use ISR only when deploying to a serverless or server-based platform.

Further Reading