Skip to main content
Adzbyte
Development

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

Adrian Saycon
Adrian Saycon
March 13, 20264 min read
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() or headers() from next/headers
  • searchParams in a page component
  • A fetch() call with cache: '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. revalidatePath expects 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 include next: { 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 explicit next.revalidate or cache options?
  • Is anything in the render path calling cookies(), headers(), or using searchParams?
  • 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.

Adrian Saycon

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.

Discussion (0)

Sign in to join the discussion

No comments yet. Be the first to share your thoughts.