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.

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
| Surprise | Why it happens | What to do |
|---|---|---|
| First visitor sees stale data | Regeneration happens after the response is sent | Accept it, or use on-demand revalidation |
| New pages appear without a deploy | dynamicParams defaults to true | This is usually what you want |
revalidate seems ignored locally | next dev re-renders every request | Test 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
revalidatevalues 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