Incremental Static Regeneration: When Rebuilding Your Whole Site Stops Making Sense
ISR lets Jamstack sites update individual pages without full rebuilds — but it trades immediate consistency for speed. A practical look at how it works, a Next.js example, and where it breaks.
23 Dec 2025, 16:40 UTC

Your marketing team edits a product description in the CMS. On a purely static Jamstack site, that one-sentence change triggers a full rebuild: every page re-rendered, every asset re-uploaded, several minutes of CI time for a typo fix. Incremental Static Regeneration (ISR) exists precisely because this trade-off stops being acceptable once a site grows past a few hundred pages.
The thesis of this piece: ISR is not "SSR but cached" — it's a distinct engineering decision with its own consistency model, platform requirements, and failure modes, and choosing it well means understanding where it breaks.
What ISR actually does
With classic static generation, every page is rendered at build time. With ISR, you attach a revalidation window to a page — say, 300 seconds. The first request after that window expires gets the stale HTML immediately (fast response, no waiting), and the platform regenerates the page in the background. The next request gets the fresh version. This is the stale-while-revalidate pattern applied to full HTML documents.
The result: you can statically "pre-build" only your most-trafficked pages and let the long tail generate lazily on first request. A 10,000-product catalog no longer means a 10,000-page build.
A worked example: the product catalog
Consider an e-commerce catalog with 10,000 SKUs. A sensible ISR strategy:
- Pre-render the top 100 sellers at build time (they must always be instant).
- Let the remaining 9,900 render on first request, then cache with a 300-second revalidate window.
- When inventory or pricing changes in the CMS, fire a webhook that invalidates only the affected pages rather than waiting out the timer.
In Next.js (App Router, version 14+), the page-level piece looks like this:
// app/products/[slug]/page.tsx
export const revalidate = 300; // seconds
export async function generateStaticParams() {
const top = await getTopSellers(100);
return top.map((p) => ({ slug: p.slug }));
}And the webhook handler that your CMS (Contentful, Sanity, Strapi — anything that can POST on publish) calls:
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(req: Request) {
const { secret, productId } = await req.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'unauthorized' }, { status: 401 });
}
revalidateTag(`product-${productId}`);
return Response.json({ revalidated: true });
}Run this on your hosting platform of choice; the webhook endpoint must be deployed where your pages are served, and the secret must be a real environment variable, not committed to the repo. The risk to note: an unprotected revalidation endpoint lets anyone force regenerations, which on metered platforms means real money.
Other frameworks offer equivalent mechanisms with different APIs: Astro uses per-route export const prerender = false plus platform adapters, and Nuxt 3 uses routeRules with swr: true. The concepts transfer; the syntax does not.
How to verify it's working
Don't trust the config — check the cache headers. Request a page twice:
curl -sI https://yoursite.com/products/some-sku | grep -i cacheOn Vercel, look at x-vercel-cache (HIT, STALE, or MISS); on Cloudflare, cf-cache-status. Then update the source content, request within the revalidate window (expect stale), wait past the window, request once to trigger regeneration, and request again to confirm fresh content. If the second request after the window still serves old HTML, your revalidate value isn't being applied — a common cause is a route-level config being overridden by a fetch-level cache option.
The trade-offs you actually sign up for
Eventual consistency. Users can see stale data for up to the revalidate window. For a blog, fine. For prices or stock levels, use tag-based on-demand invalidation or don't cache that data at all.
Platform dependence. ISR needs writable, persistent storage at the serving layer. Vercel, Netlify, and Cloudflare Pages provide this natively. Self-hosted multi-instance setups need shared storage (a mounted volume or object store), or your instances will regenerate independently and serve inconsistent content. This is a real operational requirement, not a footnote — verify it with a two-instance test before committing.
What ISR doesn't solve. User-specific content — logged-in pricing, personalized recommendations, auth-gated pages — still requires client-side fetching, SSR, or edge middleware. Many production sites end up hybrid: ISR for the shell, client fetch for the dynamic islands. That's a fine architecture, but go in knowing ISR alone won't cover it.
Preview mode. Content editors need to see drafts, which means bypassing the cache via cookies. Misconfigured preview handling can leak draft content publicly or serve stale previews — test both directions.
Deciding
Reach for ISR when your content changes more often than a full rebuild tolerates, most pages are low-traffic, and staleness of seconds-to-minutes is acceptable. Stay with pure static when the site is small and builds are fast. Go SSR when consistency must be immediate. The actionable step: pick your most-changed content type, wire up one ISR page with a webhook-driven revalidateTag, and measure time-to-consistency with curl before rolling it out site-wide.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.