Diagnosing Stale ISR Cache Issues on Vercel for Next.js
Stale ISR pages on Vercel? A step‑by‑step diagnostic guide that shows how to identify mis‑configured revalidate intervals, middleware conflicts, header mismatches, and query‑string pitfalls. Fixes and verification steps included.
03 Aug 2026, 12:08 UTC

Problem: ISR pages stay stale longer than expected
When you deploy a Next.js site to Vercel with Incremental Static Regeneration (ISR), you expect pages to refresh automatically after the revalidate interval. In practice many teams notice that the content appears unchanged for minutes or even hours, despite updates in the CMS or source data. This guide walks you through the most common causes, how to detect them, and how to fix them.
Recognizable Condition
After a content update, a browser request to an ISR page still returns the old HTML and the Cache-Control header shows stale-while-revalidate=60. Subsequent requests within the same minute keep serving the cached version. The page never shows the new data until a manual purge or a full redeploy.
Cause & Diagnostic Table
| Cause | Diagnostic Indicator |
|---|---|
| ISR revalidate interval too long or missing | Cache header shows max-age=… but no stale-while-revalidate or revalidate is 0 |
| Middleware redirect/rewrite | Request path is rewritten before ISR lookup; dev tools show 302/307 instead of 200 |
| Header mis‑alignment | vercel.json headers set Cache-Control that conflicts with Next.js revalidate |
| Query string variation | ISR cache key includes query params; identical content served with different URLs |
Missing prerender flag | Next.js config does not enable output: 'standalone' with prerender for the route |
Ordered Checks
- Verify revalidate value in
next.config.jsOpen
next.config.jsand confirm that the page component exportsexport const revalidate = 30;or the API route setsrevalidateingetStaticProps. If the value is 0 or missing, ISR will not trigger. - Inspect Vercel deploy logs
Run
vercel logs --prodand look for entries likeISR cache updated for /blog/…. Absence of such logs indicates the cache never refreshed. - Check middleware execution
Open
middleware.ts(if present). Ensure no unconditional redirect or rewrite that would bypass the ISR route. Test by temporarily commenting out middleware and redeploying. - Examine
vercel.jsonheadersIn
vercel.json, locate anyheadersblock that setsCache-Control. Verify thatstale-while-revalidatematches therevalidateinterval. - Confirm query string handling
Make two requests:
/post/123and/post/123?preview=true. If the second request serves a different cache key, the query string is part of the cache key. Decide whether to normalise URLs or adjustrevalidatelogic. - Validate cache headers in the browser
Open DevTools Network tab, reload the page, and read the
Cache-Controlheader. It should containmax-age=0, stale-while-revalidate=30or similar. A missingstale-while-revalidateindicates mis‑configuration.
Fixes Tied to Findings
- Revalidate interval set – Add
export const revalidate = 30;in the page component orrevalidate: 30ingetStaticProps. Deploy again. - Middleware bypass – Remove or guard redirects with
if (req.nextUrl.pathname.startsWith('/api')) return NextResponse.next();. Then redeploy. - Header conflict – In
vercel.json, setCache-Control: max-age=0, stale-while-revalidate=30for ISR routes. Ensure no global header overrides. - Query string handling – Use
next.config.jstrailingSlash: trueor normalise URLs in middleware. Alternatively, addcacheKey: () => req.url.split('?')[0]ingetStaticProps(if supported). - Missing
prerenderflag – Updatenext.config.jstooutput: 'standalone'and ensureprerender: truefor the route.
Concrete Example
Below is a minimal ISR page with correct configuration and a matching vercel.json header block.
// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';
import { fetchPost } from '@/lib/api';
export const revalidate = 30;
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await fetchPost(params?.slug as string);
return { props: { post } };
};
export const getStaticPaths: GetStaticPaths = async () => ({ paths: [], fallback: 'blocking' });
export default function PostPage({ post }) {
return {post.title};
}
// vercel.json
{
"headers": [
{
"source": "/blog/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "max-age=0, stale-while-revalidate=30" }
]
}
]
}
Deploy with vercel --prod. After 30 seconds, the next request should trigger a cache update. Use vercel logs --prod to confirm the ISR cache updated log entry.
Verification Checklist
- Page URL
/blog/first-postreturnsCache-Control: max-age=0, stale-while-revalidate=30. - First request shows
Cache: HITin DevTools. Second request after 31 s showsCache: UPDATEand fresh content. - Running
vercel purge --prod /blog/first-postforces immediate refresh; DevTools showsCache: MISS.
Escalation Criteria
- If after applying all fixes the page still serves stale content beyond
revalidate, contact Vercel support with logs and thevercel.jsonfile. - If middleware is required and cannot be removed, consider moving ISR logic to an API route that triggers a revalidation via the Vercel Revalidate API.
- For large sites, enable
edgeCacheinnext.config.jsto reduce stale periods; verify withvercel analytics.
Limitations & Notes
- ISR is only available on Vercel’s Serverless functions; if you deploy to a custom server, the behavior differs.
- Edge Middleware cannot use Node.js APIs; all revalidation logic must stay in
next.config.jsor API routes. - Cache headers are honored by browsers but can be overridden by CDN settings; check Vercel’s Edge cache settings if unexpected results persist.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.