Static sites are fast because everything is pre-built. Dynamic sites are fresh because every request pulls live data. For years those were the two options, and most teams had to pick a side and live with the tradeoff. Incremental Static Regeneration (ISR) was built to remove that tradeoff — letting a site serve pre-rendered, cached pages at static-site speed while still updating content in the background without a full rebuild.
ISR is a Next.js feature, though the underlying idea — stale-while-revalidate caching applied to whole pages — has influenced how other frameworks think about hybrid rendering too. This post covers how it actually works, the two main ways to trigger it, and where it fits (and doesn't) in a modern content or e-commerce site.
The Problem ISR Solves
Before ISR, a page was either:
- Statically generated at build time — extremely fast to serve, but stale until the next full deploy, which could take minutes for a large site with thousands of pages.
- Server-rendered on every request — always fresh, but slower, and every request costs compute.
Neither is great for something like a blog with a few thousand posts, a product catalog with prices that change occasionally, or a documentation site with pages that update a few times a week. Rebuilding the entire site every time one page changes doesn't scale, and rendering every page on every request adds latency and infrastructure cost for content that mostly doesn't change between requests.
ISR handles this by treating individual pages as independently cacheable and revalidatable, instead of treating the whole site as one build artifact.
How ISR Actually Works
The core mechanism follows a stale-while-revalidate pattern: Next.js serves the cached version of a page immediately, and if that cache has passed its revalidation window, it regenerates the page in the background for future requests — the current visitor still gets a fast response, not a slow one waiting on regeneration.
Here's the sequence, using Next.js's own example of a blog post route with export const revalidate = 60:
generateStaticParamsreturns the list of posts to pre-render at build time.- During
next build, a static page is generated for each post. - Every request to
/blog/1is served from cache — instant, no server work. - Once 60 seconds have passed, the next request still gets the cached (now stale) page immediately.
- In the background, Next.js regenerates a fresh version of that page.
- Once regeneration succeeds, the next request gets the updated page, and it's cached again for the following requests.
- If a path that wasn't pre-rendered at build time is requested (say,
/blog/26, a newly added post), it can be generated on-demand at request time, assumingdynamicParamsallows it — otherwise a 404 is returned for non-existent posts.
The practical effect: no single visitor ever waits on a rebuild, content staleness is bounded to your revalidation window, and you avoid rendering pages from scratch on every request for content that rarely changes.
Two Ways to Trigger Revalidation
Time-based revalidation
The simplest approach is a fixed revalidation window set per route:
export const revalidate = 3600 // invalidate every hour
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
// render posts
}
This is a good default for most content that changes on a predictable cadence — a blog, a marketing page, a pricing page that's updated occasionally. Next.js's own guidance recommends leaning toward a longer window (an hour, rather than a few seconds) unless you have a specific reason for tighter freshness, since more frequent revalidation means more background regeneration work.
The tradeoff with time-based revalidation alone: it's imprecise. If content changes right after a revalidation cycle completes, visitors could see stale data for nearly the full window before the next regeneration kicks in.
On-demand revalidation
For precise control, Next.js exposes two functions that invalidate the cache immediately in response to an event — typically a CMS webhook, a form submission, or an internal admin action — rather than waiting for a timer:
revalidatePath invalidates an entire route:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
revalidatePath('/posts')
}
revalidateTag offers more granular control by invalidating everything tagged with a specific string, which is useful when multiple pages share the same underlying data:
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog', {
next: { tags: ['posts'] },
})
// ...
}
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
revalidateTag('posts')
}
For database-backed data (an ORM query instead of a fetch call), unstable_cache lets you apply the same tag-based revalidation model to non-fetch data sources.
One nuance worth knowing: revalidatePath and revalidateTag mark the cache as invalid, but regeneration itself happens on the next request to that path — Next.js doesn't eagerly rebuild the page the instant you call the function. If a route needs to reflect a change immediately for the very next visitor with zero stale window, that's the one gap in the current on-demand model that Next.js's own docs flag as an area they're actively extending.
Using both together
The two approaches aren't mutually exclusive — a common and recommended pattern is time-based revalidation as a safety net (so content never goes stale indefinitely even if a webhook fails silently) combined with on-demand revalidation for immediate, intentional updates when content changes are known to have happened (publishing a post, updating a price, editing a CMS entry).
Practical Considerations Before You Rely on ISR
A few caveats matter once you move past a toy example:
- Node.js runtime required. ISR works with the default Node.js runtime; it isn't supported when generating a fully static export (
output: 'export'), since static exports have no server to run background regeneration. - Multiple fetches, multiple revalidate times. If a route has several
fetchcalls with differentrevalidatevalues, Next.js uses the lowest one for the page's overall ISR timing — but each individual fetch still respects its own cache duration internally. - A
revalidate: 0orno-storefetch anywhere on the route forces dynamic rendering for that whole route, opting it out of ISR entirely. - Path accuracy matters for on-demand revalidation. Middleware rewrites aren't applied to on-demand ISR requests, so you need to revalidate the actual underlying path (e.g.,
/post/1), not a rewritten public-facing URL like/post-1. - Multi-instance deployments need a shared cache handler. The default file-system cache is per-instance. If you're running multiple server instances, an on-demand revalidation call only invalidates the instance that received it — you need a custom, shared cache handler to keep instances in sync.
- Background regeneration has a compute cost. On platforms with per-request billing, the background regeneration triggered by stale-while-revalidate still runs on the instance handling the triggering request and counts as compute.
- Debugging is possible via response headers. The
x-nextjs-cacheheader exposesHIT,STALE,MISS, orREVALIDATED, which is the fastest way to confirm ISR is behaving as expected in production rather than guessing from user reports of stale content.
Where ISR Fits (and Where It Doesn't)
ISR is a strong fit for:
- Blogs and content sites — exactly the kind of high page-count, moderate-update-frequency content it was designed for.
- E-commerce catalogs — product pages that change prices or stock status periodically but don't need per-request freshness for every visitor.
- Documentation — pages tied to a CMS or Git repo where publishing an update is a known, triggerable event, making on-demand revalidation via webhook a natural fit.
It's a poor fit for:
- Truly real-time data — a live dashboard, stock ticker, or chat interface needs dynamic rendering or client-side fetching, not a cached page that's stale by definition between revalidations.
- Per-user personalized content — ISR caches a page for all visitors; content that varies meaningfully by logged-in user isn't a good candidate for the same cached HTML.
Error Handling and Resilience
One detail that matters more in production than in a demo: what happens when background regeneration itself fails — an upstream API times out, a database query errors, a CMS is temporarily unreachable. ISR's error handling is deliberately conservative: if an error is thrown while attempting to revalidate, the last successfully generated version keeps being served from cache rather than showing an error page to visitors. On the next request after the window elapses, Next.js simply retries the regeneration. This means a flaky upstream data source degrades gracefully into "slightly stale content" rather than a broken page, which is usually the right tradeoff for content-driven sites where a few extra minutes of staleness is far less costly than an outage.
This also means monitoring matters. If your on-demand revalidation depends on a CMS webhook and that webhook silently fails (a common failure mode — misconfigured secret, changed endpoint URL, rate limiting), pages will look fine but simply stop updating, with no visible error anywhere in your app. Teams that rely heavily on on-demand revalidation should add monitoring on the revalidation endpoint itself — logging successful calls, alerting on failures, and ideally adding retry logic in the CMS webhook configuration — rather than discovering the gap when a customer asks why a published change isn't showing up.
A Common Gotcha: Stale Upstream Caches
A subtler failure mode shows up when the data source behind your ISR pages has its own caching layer. If you're pulling from a headless CMS or third-party API that itself caches responses at a CDN edge, an on-demand revalidation call in your Next.js app can technically succeed — Next.js correctly marks its own cache as invalid and re-fetches — while the upstream provider still returns a stale cached response, meaning your regenerated page ends up rendering the same old content anyway. This is why checking whether your upstream data provider has caching enabled by default is worth doing early; some CMS SDKs default to caching responses (for example, a useCdn: true flag in certain headless CMS clients) and need that explicitly disabled for revalidation to actually pull fresh data rather than a cached copy of the old data.
ISR Compared to Other Rendering Strategies
It helps to place ISR against the other rendering options a framework like Next.js offers, since teams often default to whichever one they learned first rather than the one that fits the page:
| Strategy | Freshness | Speed | Best for |
|---|---|---|---|
| Static (build-time only) | Stale until next deploy | Fastest | Content that changes rarely (legal pages, marketing pages with no CMS) |
| ISR | Bounded staleness (revalidation window) | Near-static | Blogs, catalogs, docs — most content sites |
| Server-side rendering (dynamic) | Always fresh | Slower per request | Personalized or highly time-sensitive pages |
| Client-side fetching | Always fresh, after initial load | Fast shell, slower data | Dashboards, authenticated app views |
Most real sites end up mixing all four rather than picking one globally — a marketing homepage as static, a blog as ISR, an account dashboard as SSR or client-fetched. ISR's role in that mix is specifically for the large middle ground: pages numerous enough that per-request rendering is wasteful, but not static enough that a once-a-deploy build is acceptable.
The Bigger Pattern
ISR is really a specific implementation of a broader caching idea — serve stale-but-fast, refresh in the background — that shows up across web infrastructure (CDN edge caching, HTTP's own stale-while-revalidate cache-control directive). What makes ISR notable is that it applies that pattern at the page level inside the framework itself, so a team building a content-heavy site doesn't need to hand-roll a CDN invalidation pipeline or run a full rebuild pipeline to keep pages both fast and current.
For teams evaluating whether to adopt it, the practical test is simple: if your content update frequency is "sometimes, and we usually know when," ISR — ideally combined time-based and on-demand — is very likely the right default. If it's "constantly, per request, per user," you want dynamic rendering instead, and ISR isn't the tool for that job.
Sources:
Get new posts as they publish
No spam — just the next post, straight to your inbox.