Picking the Right Rendering Mode per Route: A Practical Guide to ISR in Jamstack Sites
Full rebuilds stop scaling once a Jamstack site grows. Here's how to decide which routes get ISR, which stay static, and which need server rendering — with a worked example and verification steps.
12 Feb 2026, 18:06 UTC

Your marketing site has 40 pages and rebuilds in two minutes. Then you bolt on a product catalog with 8,000 SKUs, and suddenly every typo fix in the CMS triggers a fifteen-minute full rebuild. This is the wall classic Jamstack hits: pre-rendering everything at build time stops scaling once page count or content churn grows. The useful takeaway is that you don't have to choose between "all static" and "all server-rendered" — Incremental Static Regeneration (ISR) lets you decide per route, and the decision framework is simpler than it looks.
The build-time bottleneck
Jamstack's original promise was compelling: render every page to plain HTML at build time, push it to a CDN, and serve it instantly with no origin server in the request path. That works beautifully for blogs and marketing pages. It breaks down when:
- You have thousands of pages, so each build takes long enough to delay urgent content fixes.
- Content changes frequently — a news index, a catalog with stock-driven badges — and each change means a rebuild and redeploy.
- Editors in the CMS expect their change to be live in seconds, not after a CI pipeline finishes.
ISR, popularized by Next.js, addresses this by serving a cached static page while regenerating it in the background after a configurable interval. The visitor who triggers regeneration still gets the (slightly stale) cached page instantly; the next visitor gets the fresh one. No rebuild, no redeploy.
The decision hinges on staleness tolerance
The real engineering question isn't "should we use ISR?" — it's "how stale can this page be, in the worst case?" That answer sorts your routes into three buckets:
| Route type | Staleness tolerance | Rendering mode |
|---|---|---|
| Marketing pages, "About", legal | Hours to days | Purely static (build-time) |
| Blog index, category pages, docs | Seconds to minutes | ISR with time-based revalidation |
| Product detail after a CMS edit | Near-zero, but only on change | ISR with on-demand (webhook) revalidation |
| Cart, checkout, account dashboard | Zero — per-user, per-request | Server-rendered or client-fetched |
Most mature Jamstack sites mix all of these per route. The mistake is picking one mode site-wide.
A worked example: catalog site with a 60-second window
Consider an e-commerce catalog. Category listing pages change as products are added or reordered, but a minute of staleness is harmless. In Next.js (App Router, version 13.4+ assumptions below), the route looks like this:
// app/catalog/[category]/page.tsx
// Runs in your Next.js app; no special permissions needed beyond your normal deploy.
export const revalidate = 60; // seconds — regenerate at most once per minute
export default async function CategoryPage({ params }) {
const products = await getProductsByCategory(params.category);
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}With revalidate = 60, the first request after the cache entry ages past 60 seconds triggers a background regeneration; that request itself is served the stale copy. Marketing pages in the same app omit revalidate entirely and stay build-time static. The cart uses export const dynamic = 'force-dynamic' so it renders per request.
For product detail pages where an editor's price correction should appear immediately, pair ISR with on-demand revalidation: your CMS fires a webhook on publish, hitting an API route that calls revalidatePath('/catalog/item/123'). Protect that endpoint with a shared secret token checked against the webhook payload — an unprotected revalidation endpoint lets anyone force regenerations and hammer your origin.
How to verify it's actually working
Don't trust the config — observe the behavior. A quick check:
- Create a throwaway page that renders
new Date().toISOString()at generation time, withrevalidate = 30. - Load it, note the timestamp, wait 45 seconds, reload. The first reload after expiry typically shows the old timestamp (stale-while-revalidate behavior); a second reload a moment later should show a fresh one.
- Inspect response headers. On Vercel,
x-vercel-cachereportsHIT,STALE, orMISS; other hosts expose similar cache-status headers. Confirm hits outnumber regenerations under normal traffic. - For on-demand revalidation, make a CMS edit and time how long until the change is publicly visible — it should be seconds, not the full time-based interval.
The trade-offs worth stating out loud
ISR shifts work from build time to request time, and that has real costs:
- Stale content within the window. Time-based ISR will serve outdated content for up to your interval. Never use it for inventory counts, live pricing, or anything with strict correctness requirements.
- Slower cache misses. The request that triggers regeneration may wait on data fetching on some platforms, and background regeneration still consumes origin resources. A pure static hit is always faster.
- Platform-specific semantics. ISR behavior on Vercel differs from self-hosted Next.js or other CDN setups, particularly around cache invalidation and whether concurrent traffic can trigger multiple simultaneous regenerations of the same page. Under heavy load, that "thundering herd" can spike origin load — check your host's docs and your origin metrics.
- Debugging complexity. "Why is the old content still showing?" becomes a caching question, not a deployment question. Make sure your team can read cache headers before rolling this out broadly.
Where to start
Audit your routes by staleness tolerance, not by page type or team preference. Start conservative: keep everything static except one high-churn route, give it a 60-second window, and watch the cache headers for a week. If hit rates are high and editors stop complaining about publish delays, extend the pattern. If you find yourself needing ever-shorter intervals, that route probably wants on-demand revalidation or server rendering instead — the interval you keep shrinking is telling you something.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.