next.js app router stale isr data fix
Maksudur Rahman
Pages built with the App Router and ISR are supposed to refresh after the revalidate interval. In production they sometimes keep serving the old HTML even after the source data changes. This article walks through the next.js app router stale isr data fix you can drop into a Next.js 14+ TypeScript project and see fresh content instantly.
TL;DR / Quick Summary
Set
revalidateonfetchand addcache: 'force-no-store'for critical paths.Add
cacheTagsto the fetch call and enableexperimental.cacheTaginnext.config.js.Deploy a Vercel webhook that calls
/api/revalidateafter content updates.Verify with
curl -Ithatx-nextjs-cacheheader showsMISS.
Reproducing the Stale ISR Symptom
Repo layout
my-next-app/
├─ app/
│ └─ blog/
│ └─ [slug]/
│ ├─ page.tsx # ISR page
│ └─ layout.tsx
├─ pages/
│ └─ api/
│ └─ revalidate.ts # on‑demand webhook
├─ next.config.js
└─ vercel.jsonThe page code (app/blog/[slug]/page.tsx)
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
export const revalidate = 60; // seconds
export default async function Page({ params }: { params: { slug: string } }) {
const data = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: {
tags: ['post'],
revalidate: 60,
},
}).then(res => res.ok ? res.json() : null);
if (!data) notFound();
return (
<article>
<h1>{data.title}</h1>
<p>{data.body}</p>
</article>
);
}Deploy the repo to Vercel. Update a post via the CMS, wait a few seconds, then refresh the page. If the old title still shows, you’re looking at stale ISR data.
Why ISR Stales in Production
Vercel caches the generated HTML at the edge for the revalidate window. When the origin API returns a new payload, Vercel only swaps the cache after the next request passes the interval. Two hidden culprits make the window longer than you expect:
Default
fetchcache – withoutcache: 'force-no-store'the request may be served from the Vercel edge cache, bypassing the fresh API call.Missing cache tags – Vercel’s tag‑based invalidation only works when
experimental.cacheTagis turned on innext.config.js.
Both cause the edge to keep the old HTML until the timer expires, which looks like a bug but is just a config gap.
Fix #1: Explicit Revalidate & Cache Tags
Update the fetch call to force a fresh request on every ISR run and attach a tag that you can purge on‑demand.
// app/blog/[slug]/page.tsx (updated)
const data = await fetch(`https://api.example.com/posts/${params.slug}`, {
cache: 'force-no-store', // bypass edge cache for the fetch itself
next: {
tags: ['post'],
revalidate: 30, // shorten the window for demo purposes
},
}).then(res => res.ok ? res.json() : null);Enable tag support (next.config.js)
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
cacheTag: true, // required for `next.tags`
},
};
module.exports = nextConfig;Now every ISR regeneration hits the API directly, and the tag post is attached to the generated HTML.
Fix #2: Vercel Edge Headers to Prevent Stale Reads
Add a vercel.json that tells the edge to treat the route as no-cache until the ISR rebuild finishes.
// vercel.json
{
"routes": [
{
"src": "/blog/(.*)",
"headers": {
"Cache-Control": "public, max-age=0, s-maxage=0, stale-while-revalidate=0"
}
}
]
}The header forces the CDN to fetch the fresh HTML on the next request, eliminating the visual lag.
Fix #3: On‑Demand Revalidation Webhook
For content that changes irregularly, push a webhook from your CMS that hits a Next.js API route.
// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// secret token protects the endpoint
if (req.query.secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
await res.revalidate('/blog/' + req.body.slug);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).json({ message: 'Revalidation failed' });
}
}Configure your CMS to POST { slug: 'my-post' } to https://your-site.vercel.app/api/revalidate?secret=YOUR_SECRET after publishing. The edge cache is cleared instantly, so users never see stale data.
Request Flow Diagram
flowchart TD
A[User Request] --> B{Edge Cache?
stale?}
B -- Yes --> C[Serve Cached HTML]
B -- No --> D[Run ISR (fetch API)]
D --> E[Generate HTML]
E --> F[Store in Edge Cache]
F --> C
subgraph Revalidation Hook
G[CMS Publish] --> H[POST /api/revalidate]
H --> I[res.revalidate()]
I --> F
endThe diagram shows the normal edge‑cache path and the extra hook that forces a fresh store.
Comparison Table
Strategy | Freshness Guarantee | Complexity | Ideal Use‑Case |
|---|---|---|---|
Default ISR ( | Up to | Low | Low‑traffic blogs |
Force‑no‑store + tags | No stale HTML, API hit each rebuild | Medium | Content that must be accurate |
On‑demand webhook | Immediate purge | High | CMS‑driven sites with frequent edits |
Common Pitfalls & Troubleshooting
next: { tags: [...] }ignored – make sureexperimental.cacheTagistrueinnext.config.js. Without it the tag header never appears.Vercel still serves old HTML after webhook – check that the secret matches
process.env.REVALIDATE_SECRETand that the route path (/blog/slug) matches the page URL exactly.cache: 'force-no-store'breaks static caching – use it only on ISR pages that need fresh data; applying it globally will defeat the performance benefits of static generation.revalidatevalue not respected – Node.js 20’sfetchpolyfill may cache aggressively; always setcacheoption when you rely on tags.
If you see x-nextjs-cache: HIT after a webhook call, run curl -I with Cache-Control: no-cache to force a bypass and confirm the edge state.
FAQ
How do I test ISR locally without Vercel? Run
next devand hithttp://localhost:3000/_next/data/<build-id>/blog/slug.json. Therevalidatetimer works the same way.Can I combine ISR with
getStaticPropsin the App Router? No. The App Router uses Server Components;revalidateis declared as an export or viafetchoptions.Why does
next devsometimes ignorerevalidate? Development mode disables caching by design, so you’ll always see fresh data. The issue only appears in production builds.Do Vercel Edge Functions respect
Cache-Controlheaders fromvercel.json? Yes. Theheadersblock overrides the default CDN behavior for the matched route.
Connect with KoderSolution 🚀
Master modern software engineering, scalable architectures, and advanced backend systems with KoderSolution:
🌐 Official Website: Read more architectural deep-dives and tutorials on kodersolution.com
📺 YouTube Channel: Subscribe to @kodersolution on YouTube for concise, animated system design breakdowns and shorts.
📱 Facebook Page: Follow our official Facebook Page for daily development tips, architecture discussions, and community updates.
💬 What's your setup? How does your production pipeline handle ISR invalidation—do you rely on on‑demand revalidation, shorter intervals, or a custom cache layer? Share your thoughts and join the discussion below!
Related Engineering Guides
Recommended Resources & Courses
Maksudur Rahman
Senior Developer Advocate & System Lead Moderator.