Implementing Incremental Static Regeneration in Jamstack: A Platform-Agnostic Guide
A practical guide to implementing Incremental Static Regeneration (ISR) in Jamstack sites: configure page-level revalidation, deploy to Vercel/Netlify/Cloudflare, secure CMS webhooks for on-demand updates, verify cache headers, and recover when ISR fails.
24 Oct 2025, 04:35 UTC

The Problem: Static Sites Can't Stay Fresh Without Full Rebuilds
Traditional Jamstack sites regenerate every page at build time. When content changes in a headless CMS, you face a choice: trigger a full-site rebuild (slow, resource-heavy) or serve stale content until the next scheduled build. Incremental Static Regeneration (ISR) solves this by letting individual pages revalidate on-demand or after a time-to-live (TTL), serving cached HTML immediately while fresh content generates in the background.
Takeaway: Configure page-level revalidation, deploy to an ISR-capable platform, and wire CMS webhooks to on-demand revalidation endpoints. Test cache headers and fallback behavior on your target platform—local behavior differs.
Prerequisites
- Framework with ISR support: Next.js 9.5+ (via
getStaticProps), Astro 2.0+ (hybrid output withgetStaticPaths), or Nuxt 3 (Nitro engine). - Deployment target with ISR runtime: Vercel, Netlify, Cloudflare Pages, or self-hosted (Docker + Redis/Nginx cache layer).
- Headless CMS or data source that emits webhook events on publish/update (Contentful, Sanity, Strapi, WordPress, etc.).
- Shared secret for securing webhook-to-platform communication.
Step 1: Configure Page-Level Revalidation
In Next.js, export a revalidate constant (seconds) from getStaticProps. This sets the minimum TTL before a background regeneration can occur.
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return {
props: { post },
revalidate: 60, // regenerate at most once per 60 seconds
};
}
export async function getStaticPaths() {
const paths = await fetchAllSlugs();
return { paths, fallback: 'blocking' };
}In Astro, set revalidate in getStaticPaths:
// src/pages/blog/[slug].astro
export async function getStaticPaths() {
const posts = await fetchPosts();
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
export const revalidate = 300; // 5-minute minimum TTLNote: revalidate is a minimum TTL. Actual regeneration happens on the next request after TTL expires. Low-traffic pages may serve stale content longer than the configured value.
Step 2: Deploy to an ISR-Capable Platform
Push to your Git provider and connect the repository to Vercel, Netlify, or Cloudflare Pages. Each platform implements ISR differently:
- Vercel: Persistent serverless functions with shared in-memory cache.
revalidatemaps directly toCache-Control: s-maxage=N, stale-while-revalidate=M. - Netlify: Edge Functions with distributed cache. Uses
stale-while-revalidatevia Netlify's CDN. - Cloudflare Pages: Workers KV for cache storage. Revalidation runs in a Worker.
Do not assume local next dev or astro preview replicates production ISR behavior. Test on the target platform.
Step 3: Set Up On-Demand Revalidation via CMS Webhook
Configure your CMS to POST to the platform's revalidation endpoint on content publish. Example for Vercel:
# Vercel on-demand revalidation endpoint
POST https://your-site.vercel.app/api/revalidate
?secret=SHARED_SECRET_TOKEN
&path=/blog/post-1Create a serverless route to handle the webhook (Next.js example):
// pages/api/revalidate.js
export default async function handler(req, res) {
const { secret, path } = req.query;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
await res.revalidate(path);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Revalidation failed');
}
}Security: Store the secret in platform environment variables, never in code. Validate the payload signature if your CMS provides one (e.g., Contentful's X-Contentful-Signature header).
Step 4: Batch Webhook Payloads for Bulk Updates
Platform rate limits apply: Vercel allows ~1,000 revalidation requests/minute per project. For bulk publishes (e.g., 50 blog posts at once), modify your CMS webhook to send an array of paths and process them in a loop with a small delay:
// pages/api/revalidate-bulk.js
export default async function handler(req, res) {
const { secret, paths } = req.body;
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
const results = [];
for (const path of paths) {
try {
await res.revalidate(path);
results.push({ path, status: 'revalidated' });
await new Promise(r => setTimeout(r, 50)); // throttle
} catch (e) {
results.push({ path, status: 'failed', error: e.message });
}
}
res.json({ results });
}Expected Checks: Verify It Works
1. Cache-Control Headers
Run curl -I https://your-site.com/blog/post-1 and confirm:
Cache-Control: s-maxage=60, stale-while-revalidate=300Values should match your revalidate configuration. If a CDN or proxy strips or overrides these headers, ISR breaks silently.
2. Webhook Trigger Logs
In Vercel: vercel logs --follow or dashboard Functions tab. In Netlify: netlify functions:log. Confirm webhook POSTs appear within seconds of CMS publish.
3. Concurrent Requests During Regeneration
Simulate traffic while a page regenerates:
# Trigger revalidation, then hammer the page
curl -X POST "https://your-site.com/api/revalidate?secret=TOKEN&path=/blog/post-1"
vegeta attack -rate=50 -duration=10s -targets=targets.txt | vegeta reportWhere targets.txt contains GET https://your-site.com/blog/post-1. Expect zero 5xx errors; all requests should serve cached HTML (stale) while regeneration completes.
4. Fallback Behavior for Unbuilt Paths
With fallback: 'blocking' (Next.js) or fallback: true (Astro), a request for a not-yet-generated path blocks until HTML renders, then caches. With fallback: true alone, the first request sees a loading shell. Choose based on UX requirements—blocking adds latency but no flash of fallback UI.
Recovery Options When ISR Fails
Fallback to Full Rebuild
If ISR stops working (platform outage, cache corruption), trigger a manual full rebuild via your CI/CD pipeline. Keep the build script accessible and tested.
Stop Runaway Revalidation Loops
A misconfigured webhook can trigger infinite revalidation. Disable the webhook in the CMS immediately, then purge the edge cache:
# Vercel
vercel purge --scope=team-name project-name
# Netlify
netlify api purgeCache --siteId=SITE_ID
# Cloudflare Pages
wrangler pages deployment purge --project-name=PROJECTStale Content Persists Beyond TTL
Check: (1) revalidate value matches CMS update frequency, (2) no CDN layer overrides Cache-Control with longer TTLs, (3) the page receives enough traffic to trigger regeneration (remember: ISR is request-driven).
Platform-Specific Cautions
| Platform | Cache Mechanism | Key Limitation |
|---|---|---|
| Vercel | In-memory (shared across invocations) | Cold starts on low-traffic paths; 1000 req/min revalidation limit |
| Netlify | Distributed edge cache | Edge Function timeout 30s; cache inconsistency across regions |
| Cloudflare Pages | Workers KV (eventually consistent) | KV write latency; 1000 writes/day free tier limit |
| Self-hosted | Redis / Nginx proxy_cache | You build the revalidation logic, cache invalidation, and scaling |
Self-hosted ISR is not trivial. You need a cache layer (Redis, Nginx), a background worker to regenerate pages, and logic to serve stale content while regenerating. Most teams should use a managed platform.
Verification Checklist for Production
- Deploy a test page with
revalidate: 10. - Update CMS content, trigger webhook.
curl -Ithe page repeatedly; confirm updated content appears andCache-Controlheaders persist.- Run
vercel inspectornetlify functions:logto see revalidation function duration and invocation count. - Load-test with
vegetaork6during regeneration; verify zero downtime.
Limitations to Accept
- ISR is request-driven: zero traffic = zero regeneration, regardless of TTL.
revalidateis a floor, not a guarantee. A page with one visitor per hour onrevalidate: 60serves stale content for ~1 hour.- On-demand revalidation endpoints have rate limits. Batch bulk updates.
- Platform behavior diverges. What works on Vercel may behave differently on Netlify or Cloudflare. Test on target infrastructure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.