Rishabh Parsediya

← Back to blog
Next.js2 min read

What ISR Actually Caches (And When It Lies To You)

Next.js revalidate is not a rebuild timer. Understanding stale-while-revalidate explains why your first visitor after the window still sees old data — and why that's the right tradeoff.

What ISR Actually Caches (And When It Lies To You)

Next.js gives you incremental static regeneration with a single line, and that line hides more behaviour than most people expect. Here's what revalidate actually does, and the three places it surprises you.

What revalidate really means

export const revalidate = 300 does not mean "rebuild this page every five minutes". It means: serve the cached page; if a request arrives more than 300 seconds after the page was generated, serve the stale version anyway and regenerate in the background.

That last part is the one people miss. The visitor who trips the boundary gets the old page. The fresh one is ready for whoever comes next.

// app/blog/page.tsx
export const revalidate = 300;

export default async function BlogIndex() {
  const posts = await listPublishedPosts();
  return <PostList posts={posts} />;
}

Why stale-while-revalidate is the right default

The alternative — blocking the request while you regenerate — turns a cache miss into a slow page load for a real person. Serving stale content keeps latency flat and moves the cost off the critical path.

The three surprises

SurpriseWhy it happensWhat to do
First visitor sees stale dataRegeneration happens after the response is sentAccept it, or use on-demand revalidation
New pages appear without a deploydynamicParams defaults to trueThis is usually what you want
revalidate seems ignored locallynext dev re-renders every requestTest caching with next build && next start

That third one costs people the most time. Development mode does not use the ISR cache at all, so any caching behaviour you're trying to verify has to be checked against a production build.

On-demand revalidation is the real answer

Time-based revalidation is a fallback. If you know when content changes — because a publish button was clicked — call revalidatePath and skip the guessing entirely:

import { revalidatePath } from "next/cache";

export async function publishPost(slug: string) {
  await db.update(posts).set({ status: "published" }).where(eq(posts.slug, slug));
  revalidatePath("/blog");
  revalidatePath(`/blog/${slug}`);
}

Keep the time-based revalidate values anyway. They cost nothing and they cover the case where content changes outside your app — a direct database edit, for instance.

Once both are in place, the numbers you picked for revalidate stop mattering very much, which is a good sign you've got the design right.

  • nextjs
  • caching
  • performance
  • isr