Incremental Static Regeneration (ISR)
ISR gives you the best of both worlds: static performance with fresh data. Pages are cached but automatically revalidate.
How ISR Works
- First request: Serve cached page, trigger background revalidation
- Background: Generate new version of the page
- Next request: Serve the new cached version
This is called "stale-while-revalidate" - users always get a fast response.
Time-Based Revalidation
Set revalidation at the page or fetch level:
// Page-level: All fetches revalidate every 60 seconds
export const revalidate = 60
export default async function Page() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json())
return <PostList posts={posts} />
}
// Fetch-level: This specific fetch revalidates every 300 seconds
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 300 }
})
On-Demand Revalidation
Revalidate when data changes, not on a timer:
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function publishPost(postId: string) {
await db.posts.update({
where: { id: postId },
data: { published: true }
})
// Revalidate the blog listing
revalidatePath('/blog')
// Revalidate this specific post
revalidatePath(`/blog/${postId}`)
}
Tag-Based Revalidation
Group related fetches with tags:
// Fetch with a tag
async function getBlogPosts() {
return fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
}).then(res => res.json())
}
async function getPost(slug: string) {
return fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: ['posts', `post-${slug}`] }
}).then(res => res.json())
}
// Revalidate all posts
revalidateTag('posts')
// Revalidate a specific post
revalidateTag('post-hello-world')
Via API Route (Webhook Pattern)
Trigger revalidation from external services:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidation-secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 })
}
const { path, tag } = await request.json()
if (path) {
revalidatePath(path)
}
if (tag) {
revalidateTag(tag)
}
return Response.json({ revalidated: true })
}
Call from your CMS webhook:
curl -X POST https://yoursite.com/api/revalidate \
-H "x-revalidation-secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"path": "/blog"}'
ISR with Dynamic Routes
Combine generateStaticParams with ISR:
// Pre-generate existing posts at build
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ slug: post.slug }))
}
// New posts are generated on first request
export const dynamicParams = true
// All posts revalidate hourly
export const revalidate = 3600
When to Use ISR
| Scenario | Strategy |
|---|---|
| Blog posts, rarely updated | revalidate = 3600 (hourly) |
| E-commerce products | On-demand revalidation when stock changes |
| News feed | revalidate = 60 (every minute) |
| User-specific data | SSR (not ISR) |
Summary
- ISR combines static performance with fresh data
- Use
export const revalidate = secondsfor time-based - Use
revalidatePath()orrevalidateTag()for on-demand - Tags work across multiple pages sharing the same data
- Users always get fast responses - regeneration happens in background

