Debugging Next.js ISR: When Pages Won’t Revalidate

I updated a blog post in my CMS, waited five minutes, refreshed the page — and nothing changed. The old content was still there. I bumped the revalidation interval, deployed again, cleared my browser cache. Still stale. If you’ve used ISR in Next.js, you’ve probably been here.
The problem is rarely ISR itself. It’s almost always a misunderstanding of how caching layers interact in the App Router.
How ISR Actually Works
Incremental Static Regeneration serves a cached static page and regenerates it in the background after a specified interval. When a user hits the page after the revalidation window, they get the stale version while Next.js rebuilds the page. The next visitor gets the fresh one.
In the App Router, you control this with the revalidate export:
// app/blog/[slug]/page.tsx
export const revalidate = 60; // seconds
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetchPost(params.slug);
return <Article post={post} />;
}
This tells Next.js: “Cache this page for 60 seconds, then regenerate it on the next request after that window.” Simple enough. But several things can prevent it from working.
Problem 1: Fetch Cache Overriding Page Revalidation
This is the most common gotcha. In the App Router, fetch() calls have their own caching behavior that’s independent of the page-level revalidate value.
// This fetch is cached indefinitely by default!
const res = await fetch('https://api.example.com/posts/my-post');
// You need to set revalidation on the fetch too
const res = await fetch('https://api.example.com/posts/my-post', {
next: { revalidate: 60 },
});
If your fetch doesn’t specify next.revalidate or cache: 'no-store', the response is cached at the fetch level forever. Your page regenerates on schedule, but it keeps using the same stale API response.
The fix: explicitly set next: { revalidate } on every fetch call, or use cache: 'no-store' for data that must always be fresh.
Problem 2: Dynamic Rendering Disabling ISR
Certain operations force Next.js into dynamic rendering mode, which bypasses ISR entirely. If your page uses any of these, it won’t be statically generated:
cookies()orheaders()fromnext/headerssearchParamsin a page component- A
fetch()call withcache: 'no-store' export const dynamic = 'force-dynamic'
I once had a layout component that read a cookie for theme preference. That single cookies() call made every page under that layout dynamically rendered. ISR was silently disabled across the entire blog section.
Problem 3: On-Demand Revalidation Mistakes
On-demand revalidation lets you purge cached pages immediately instead of waiting for the time interval. You do this with revalidatePath or revalidateTag:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { path, tag, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
if (tag) {
revalidateTag(tag);
} else if (path) {
revalidatePath(path);
}
return NextResponse.json({ revalidated: true, now: Date.now() });
}
Common mistakes with this approach:
- Wrong path format.
revalidatePathexpects the route path (/blog/my-post), not the file path (/app/blog/[slug]/page.tsx). - Missing tag registration.
revalidateTag('posts')only works if your fetch calls includenext: { tags: ['posts'] }. - Not awaiting the webhook. If your CMS fires a webhook to the revalidation endpoint but doesn’t wait for the response, you won’t see errors.
Problem 4: CDN Caching in Front of Next.js
If you’re deploying to Vercel, the CDN layer is handled automatically. But if you’re self-hosting behind Cloudflare, Nginx, or another CDN, there’s a second cache layer between your users and Next.js.
Your Next.js app might be regenerating pages correctly, but the CDN is serving its own stale copy. Check your CDN cache headers:
# Check what cache headers your page returns
curl -I https://yoursite.com/blog/my-post | grep -i cache
Look for Cache-Control, CDN-Cache-Control, and x-cache headers. If the CDN TTL is longer than your ISR revalidation interval, the CDN wins.
Debugging Checklist
When ISR isn’t working, run through this:
- Is the page actually statically rendered? Check the build output for the route.
- Do all
fetch()calls have explicitnext.revalidateorcacheoptions? - Is anything in the render path calling
cookies(),headers(), or usingsearchParams? - For on-demand revalidation: are your tags registered on the fetch calls?
- Is a CDN caching the response independently?
- In development, ISR doesn’t behave like production. Always test with
next build && next start.
The fundamental insight is that the App Router has multiple independent cache layers — fetch cache, page cache, router cache, and potentially CDN cache. ISR only controls the page cache. If your content is stale, you need to figure out which layer is holding onto old data.
Once you internalize that model, ISR debugging goes from mysterious to mechanical.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


