Suggest an editImprove this articleRefine the answer for “How incremental static regeneration (ISR) works in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**ISR (Incremental Static Regeneration)** - a Next.js strategy that pre-renders pages at build time and regenerates individual pages in the background after a `revalidate` timer expires, without rebuilding the whole app. ```tsx // App Router: cache for 5 minutes, regen on next post-expiry request const res = await fetch('https://api.example.com/data', { next: { revalidate: 300 } }); ``` **Key:** the first request after the timer expires gets the stale page; the next request gets the fresh version. If `getStaticProps` fails during background regen, the stale page keeps serving.Shown above the full answer for quick recall.Answer (EN)Image**ISR (Incremental Static Regeneration)** - a Next.js strategy that pre-renders pages statically at build time, then regenerates individual pages in the background after a `revalidate` timer expires, without touching the rest of the app. ## Theory ### TL;DR - Think of a coffee shop with pre-brewed pots. The pot is ready on the counter (static), but when it runs low the machine starts a new brew automatically. You get what was already poured; the next customer gets fresh coffee. - Main difference: SSG requires a full `next build` to update content. ISR updates individual pages on a timer with no rebuild. SSR re-renders on every request; ISR serves from cache. - Decision rule: data changes every few minutes to a few hours and most requests hit the same pages? ISR fits. Data is user-specific? SSR. Never changes? Plain SSG. - First request after the timer expires gets the stale version. The *next* request gets fresh content. - On-demand revalidation (`revalidatePath`, `revalidateTag`) is more precise than a timer: you push updates exactly when data changes. ### Quick example ```tsx // pages/posts.js - basic ISR, Next.js 12+ export async function getStaticProps() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return { props: { posts }, revalidate: 60 // re-generate at most once per 60 seconds }; } export default function Posts({ posts }) { return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } // t=0: static HTML served from CDN. // t=61s, request 1: gets stale HTML, getStaticProps fires in background. // t=61s, request 2: gets freshly generated page. ``` The `revalidate: 60` field is serialized into the static JSON payload at build time. Next.js reads it on every cache check and decides whether to queue a background regen. ### Key difference ISR is SSG with a built-in expiry hook. At build, `getStaticProps` runs and Next.js writes `/_next/static/[page].json` plus the HTML shell to disk or CDN. That JSON also carries the `revalidate` value. When a request arrives after the timer expires, the Node.js server queues a background `getStaticProps` call, writes new HTML and JSON atomically via `fs.promises`, and keeps serving stale until the write completes. No downtime, no full rebuild, no user ever waits. ### When to use - Data updates hourly or daily (blog posts, product catalogs, documentation) - ISR over SSG - High traffic, low change frequency - ISR over SSR (no per-request render cost) - Real-time or per-user data - SSR instead - Content that never changes - pure SSG, ISR adds no value here - Frequent changes plus user-specific content - SSR or client-side fetching ### Comparison table | | SSG | ISR | SSR | |---|---|---|---| | Generation | At build | At build + background | Every request | | First load | Instant (CDN) | Instant (CDN) | ~100-500ms server | | Updates | Full `next build` | Background on timer | Every request | | Stale reads | Never | Yes, until regen completes | Never | | Use when | Docs, marketing | News, catalogs, dashboards | Profiles, carts | ### How Next.js handles regeneration internally At build, `getStaticProps` serializes props to `/_next/static/[page].json` with a `revalidate` field and deploys HTML to CDN. On a post-expiry request (checked via Vercel Edge headers or a Node cache timestamp), Next.js does four things in order: 1. Serves the stale page immediately to the current user 2. Queues `getStaticProps` on a worker thread 3. Writes new JSON and HTML atomically to the filesystem or CDN edge 4. Serves stale to all concurrent requests during that window If `getStaticProps` throws during background regen, Next.js logs the error and keeps the last successful stale version alive. The page stays up. ### ISR in App Router In App Router the `revalidate` option moves into the `fetch` call itself: ```tsx // app/problems/page.tsx export default async function ProblemsPage() { const res = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } // 5-minute cache window }); const problems = await res.json(); return ( <ul> {problems.map((p: { id: string; name: string }) => ( <li key={p.id}>{p.name}</li> ))} </ul> ); } ``` You can also set it at the route segment level: ```tsx // app/problems/layout.tsx export const revalidate = 300; export default function ProblemsLayout({ children }: { children: React.ReactNode }) { return <>{children}</>; } ``` For on-demand updates, use `revalidatePath` or `revalidateTag`. In practice, I find this pattern far more useful than a timer: you invalidate exactly when a CMS webhook fires, not after a fixed window. ```tsx // app/api/revalidate/route.ts import { revalidatePath, revalidateTag } from 'next/cache'; import { NextResponse } from 'next/server'; export async function POST(request: Request) { const { secret, path } = await request.json(); if (secret !== process.env.REVALIDATION_SECRET) { return NextResponse.json({ error: 'Invalid secret' }, { status: 401 }); } revalidatePath(path); return NextResponse.json({ revalidated: true }); } ``` Tag-based invalidation works across multiple pages at once: ```tsx // Tag your fetch const res = await fetch('https://api.itlead.org/problems', { next: { tags: ['problems'] } }); // Invalidate all pages that used that tag revalidateTag('problems'); ``` ### Common mistakes **Setting `revalidate: 0` expecting per-request rendering** ```tsx // Wrong - this is not getServerSideProps return { props: { data }, revalidate: 0 }; ``` `revalidate: 0` does not give you SSR behavior. The page is still static and attempts to regenerate on every request, but remains subject to caching layers. Use `getServerSideProps` for true per-request rendering. **Mutating module-level state inside `getStaticProps`** ```tsx // Wrong let cache: string[] = []; export async function getStaticProps() { cache.push(await fetchData()); // lost between Lambda invocations } ``` `getStaticProps` runs in isolated Lambda or VM contexts. Module-level variables do not survive between calls. Use Redis, a database, or `unstable_cache()` in Next.js 14+ for shared state. **Ignoring TTFB spikes from `fallback: 'blocking'` under load** ```tsx export async function getStaticPaths() { return { paths: [{ params: { slug: 'post1' } }], fallback: 'blocking' }; } ``` With `fallback: 'blocking'`, unknown slugs are rendered server-side on first request, then cached as ISR. That first render is blocking and serial. Under traffic spikes for new slugs, p99 TTFB jumps. Pre-build popular paths or use `fallback: true` with a loading state for lower latency. **Long-running `getStaticProps` over 10 seconds** Vercel kills build-time and request-time executions after 10 seconds. If your CMS returns 500 items, paginate. For large sites, add `experimental: { workerThreads: true }` in `next.config.js` and split fetches. **Preview mode without `revalidate: false`** When preview mode is active, the `revalidate` timer is ignored, but draft content can still leak into the CDN cache. Return `revalidate: false` explicitly: ```tsx if (preview) return { props: { draftData }, revalidate: false }; ``` ### Real-world usage - Vercel blog: `revalidate: 60`, posts update without a deploy - Stripe docs: product changelogs via a Git-based CMS, `revalidate: 3600` - Hashnode: blog feeds use time-based ISR, new author pages use `fallback: blocking` - Lee Robinson's blog: talks list fetched from Airtable, regenerated via ISR - TinaCMS + Next.js starter: ISR combined with preview mode for headless CMS editing ### Follow-up questions **Q:** How does ISR handle concurrent requests during regeneration? **A:** All requests during regeneration get the stale page. Next.js uses atomic `fs.writeFile` plus ETag checks to avoid race conditions. Traffic shifts to the new version only after the write completes. **Q:** What is the difference between `revalidate: 60` in `getStaticProps` and `revalidatePath()`? **A:** `revalidate` is time-based: the page regenerates at most once per N seconds after the first post-expiry request. `revalidatePath('/path')` is on-demand: you call it explicitly from a Route Handler or Server Action, and the cache clears immediately regardless of any timer. **Q:** What happens if `getStaticProps` throws during background regeneration? **A:** Next.js logs the error and keeps serving the last successful stale version. It does not fall back to SSR. The page stays alive until a regeneration succeeds. **Q:** Does ISR work the same way on self-hosted Node vs Vercel? **A:** The mechanism is the same, but self-hosted requires a persistent filesystem or a custom cache handler for multi-instance setups. Vercel auto-scales workers and shares the cache across instances via its Edge Network with no extra config. **Q (senior):** In App Router (Next.js 14+), how does `{ next: { revalidate: 60 } }` on a fetch differ from `revalidatePath`? What cache layers are involved? **A:** `fetch` revalidation targets the Data Cache (per-request cached responses stored server-side). `revalidatePath` invalidates the Full Route Cache (the rendered RSC payload plus HTML) and also purges the client-side Router Cache. If you only call `revalidatePath`, stale `fetch` responses can still be served from the Data Cache until their own timer expires. For full freshness you often need both: `revalidateTag` on the fetch side, `revalidatePath` for the route cache. **Q (senior):** How would you scale ISR across 10,000 pages with varying revalidate intervals? **A:** Skip pre-building all 10k at deploy time. Use dynamic segments with `fallback: 'blocking'` so pages are built on first request and cached as ISR. Set shorter `revalidate` on high-traffic routes and longer on the long tail. On self-hosted setups, configure a Redis-based custom cache handler to avoid filesystem contention across multiple Node instances. Monitor regen queue depth via Vercel Analytics or a custom metric. ## Examples ### Basic: blog post list with time-based ISR (Pages Router) ```tsx // pages/posts.tsx interface Post { id: number; title: string; } export async function getStaticProps() { const posts: Post[] = await fetch('https://api.example.com/posts') .then(r => r.json()); return { props: { posts }, revalidate: 60 }; } export default function Posts({ posts }: { posts: Post[] }) { return ( <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> ); } // t=0: static HTML, CDN. // t=61s request 1: gets stale, background regen starts. // t=61s request 2: gets freshly generated page. ``` The `revalidate: 60` in the return value is the entire ISR contract with Next.js. Everything else is standard `getStaticProps`. ### Intermediate: product catalog from Contentful with 5-minute revalidation and safe preview mode ```tsx // pages/products.tsx import { createClient } from 'contentful'; interface Product { sys: { id: string }; fields: { name: string; price: number }; } export async function getStaticProps({ preview = false }: { preview?: boolean }) { const client = createClient({ space: process.env.CONTENTFUL_SPACE_ID!, accessToken: preview ? process.env.CONTENTFUL_PREVIEW_TOKEN! : process.env.CONTENTFUL_ACCESS_TOKEN!, }); const entries = await client.getEntries<any>({ content_type: 'product', limit: 20, }); // Preview mode: never cache draft content if (preview) { return { props: { products: entries.items }, revalidate: false }; } return { props: { products: entries.items }, revalidate: 300 // 5 minutes covers most price/stock update cadences }; } export default function Products({ products }: { products: Product[] }) { return ( <div> {products.map(p => ( <div key={p.sys.id}> <h2>{p.fields.name}</h2> <p>${p.fields.price}</p> </div> ))} </div> ); } ``` Without `revalidate: false` in preview mode, draft content can slip into the CDN cache and reach real users. This pattern from the ISR docs is easy to miss and shows up in production incidents more often than you would expect. ### Advanced: dynamic slugs with `fallback: 'blocking'` plus webhook-driven on-demand revalidation (App Router) ```tsx // app/articles/[slug]/page.tsx interface Article { slug: string; title: string; body: string; } // Pre-build only top 10 at deploy; unknown slugs render on first request export async function generateStaticParams() { const articles: Article[] = await fetch( 'https://api.example.com/articles?limit=10' ).then(r => r.json()); return articles.map(a => ({ slug: a.slug })); } export const dynamicParams = true; // allow unknown slugs export const revalidate = 10; // timer as a safety net export default async function ArticlePage({ params }: { params: { slug: string } }) { const article: Article = await fetch( `https://api.example.com/articles/${params.slug}`, { next: { tags: [`article-${params.slug}`] } } // tag for targeted invalidation ).then(r => r.json()); return ( <article> <h1>{article.title}</h1> <p>{article.body}</p> </article> ); } ``` ```tsx // app/api/revalidate/route.ts - called from CMS webhook on publish import { revalidateTag } from 'next/cache'; import { NextResponse } from 'next/server'; export async function POST(request: Request) { const { secret, slug } = await request.json(); if (secret !== process.env.REVALIDATION_SECRET) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } revalidateTag(`article-${slug}`); return NextResponse.json({ revalidated: true, slug }); } ``` The first request for an unknown slug blocks server-side (SSR-like) and then gets cached as an ISR page. After that, the CMS fires the webhook on publish and `revalidateTag` clears the cache immediately. The 10-second timer acts as a fallback if the webhook fails. Both mechanisms working together give you real-time updates without giving up static speed for known pages.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.