Vercel ISR with Next.js App Router: Time‑Based and On‑Demand Revalidation in Practice
Learn how Vercel's Incremental Static Regeneration works with the Next.js App Router: configure time‑based revalidation, add instant on‑demand updates via webhooks, understand the stale‑while‑revalidate trade‑off, and verify the behavior with concrete commands.
19 Dec 2025, 02:58 UTC

The problem: stale data on a static‑fast site
You want the speed of a fully cached page, but the content changes often enough that a static build every deploy is impractical. Incremental Static Regeneration (ISR) on Vercel solves this by serving a cached HTML response while a background job refreshes it after a configurable interval—or instantly when you tell it to.
How ISR works on Vercel’s infrastructure
When a page is marked for ISR, Vercel stores the rendered HTML in its edge cache. The first request after the revalidate window expires is served the stale copy while a serverless (or Fluid) function regenerates the page in the background. The next request receives the fresh version. This stale‑while‑revalidate semantic is the core trade‑off: a brief window where users may see outdated data.
In the App Router you enable time‑based revalidation two ways:
- Export a constant on the route segment:
export const revalidate = 300(seconds). - Pass
next: { revalidate: 300 }to afetchcall inside the component.
On‑demand revalidation uses revalidatePath or revalidateTag from next/cache. You typically call these from a webhook (e.g., CMS publish event) or a server action, so the cache is purged immediately rather than waiting for the interval.
Worked example: product catalog with instant publish updates
Assume a Next.js 14 App Router project deployed on Vercel. The catalog page lives at app/products/page.tsx.
// app/products/page.tsx
import { fetchProducts } from '@/lib/cms';
export const revalidate = 300; // 5‑minute time‑based window
export default async function ProductsPage() {
const products = await fetchProducts({ tags: ['products'] });
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} – ${p.price}</li>
))}
</ul>
);
}
The fetchProducts helper adds a next: { tags: ['products'] } option so the response is tagged.
// lib/cms.ts
export async function fetchProducts(opts: { tags?: string[] } = {}) {
const res = await fetch(`${process.env.CMS_URL}/products`, {
next: { tags: opts.tags, revalidate: 300 },
headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
});
return res.json();
}
When an editor publishes a change in the CMS, the CMS fires a webhook to /api/revalidate (a Route Handler).
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const secret = req.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
}
revalidateTag('products');
return NextResponse.json({ revalidated: true, now: Date.now() });
}
Result: the catalog page is never older than five minutes, and a publish event makes the new data visible on the very next request.
Trade‑offs and limitations
- Stale window: The first request after the interval serves the old HTML. If your business cannot tolerate any staleness (e.g., pricing that must be exact), ISR is not the right tool.
- Function cost: Each regeneration runs as a serverless/Fluid invocation. A high‑traffic page with a short
revalidate(e.g., 10 s) can generate thousands of function calls per minute, increasing Vercel compute spend and load on your origin CMS. - Cache reset on deploy: A new Vercel deployment clears the data cache, so the first request after a deploy always triggers a fresh render. Plan for a brief cold‑start spike after each release.
- Self‑hosted parity: ISR behavior on a custom Node server differs (no edge cache, different revalidation mechanics). Do not assume the same guarantees outside Vercel.
Verification steps you can run today
- Deploy a test page with a 30‑second
revalidateand a server‑rendered timestamp:
Run// app/test-isr/page.tsx export const revalidate = 30; export default function TestISR() { return <p>Rendered at: {new Date().toISOString()}</p>; }vercel deploy(or push to the connected Git repo). - Confirm time‑based refresh:
curl -I https://your‑project.vercel.app/test-isrrepeatedly. The timestamp should stay identical for ~30 s, then change on the next request after the interval. - Trigger on‑demand revalidation: Call the webhook endpoint with the secret:
The immediate nextcurl -X POST "https://your-project.vercel.app/api/revalidate?secret=YOUR_SECRET"curlto the test page should show a new timestamp. - Inspect headers and logs: Look for
x-vercel-cachevalues (HIT,STALE,MISS) in the response headers and check the Vercel Function logs for a regeneration entry after the interval or webhook.
These steps verify that both revalidation paths behave as documented for the exact Next.js version you pinned (e.g., next@14.2.0).
Actionable takeaway
Use ISR when you can accept a bounded staleness window and want static‑like performance with periodic freshness. Combine a modest revalidate (minutes, not seconds) with on‑demand revalidateTag driven by authenticated webhooks for instant updates on publish. Model the expected regeneration frequency against Vercel’s function pricing before committing to aggressive intervals, and always test the cache headers in a staging deployment before rolling to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.