Skip to main content
paulund

4 min read

#nextjs#isr#caching

Incremental Static Regeneration Explained

Static sites are fast. Dynamic sites are flexible. ISR is the attempt to have both, and for most content-driven applications it's the right default.

The idea is simple: render a page statically at build time, serve that static version to everyone, and regenerate it in the background when it's stale. If a page was not pre-rendered at build time and is generated on first request, that first request can still wait for a server render. After that, revalidation happens in the background, so users typically get the cached or stale page while fresh content is generated. You pay the render cost once per revalidation window, not once per request.

The Spectrum from Static to Dynamic

It helps to think of Next.js rendering as a spectrum rather than a binary choice.

  • Fully static: generateStaticParams builds every page at deploy time. Content updates require a new deploy.
  • ISR: Pages are built at deploy time (or on first request) and regenerated in the background on a schedule or on demand. No deploy needed to refresh content.
  • Fully dynamic: export const dynamic = 'force-dynamic'. Every request renders fresh. Maximum flexibility, slower time to first byte, no CDN caching benefit.

ISR sits in the middle. You get CDN-cached responses and background freshness without re-deploying for every content change.

Setting a Revalidation Window with fetch()

The most common way to use ISR is through the revalidate option on fetch():

// Revalidate at most once every 60 seconds
const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 },
})
const posts = await res.json()

Next.js caches the response and revalidates it in the background after the window expires. The first request after expiry still gets the stale data while Next.js kicks off a fresh fetch behind the scenes. The next request gets the updated result. This is stale-while-revalidate, borrowed from HTTP cache semantics and applied to server-rendered pages.

You can also set the revalidation window at the route segment level, which applies to all fetches in that segment:

// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Revalidate the whole page once per hour

Route-level config is useful when you want consistent behaviour across a page and don't want to thread a revalidate value through every individual fetch call.

On-Demand Revalidation

Time-based revalidation has a problem: if you publish new content, you have to wait for the window to expire before users see it. On-demand revalidation solves that.

Next.js exposes two functions for this: revalidatePath and revalidateTag.

// In a Server Action or route handler
import { revalidatePath, revalidateTag } from 'next/cache'

// Purge a specific page
revalidatePath('/blog/my-new-post')

// Purge all cached data tagged with 'posts'
revalidateTag('posts')

To use revalidateTag, you tag your fetch calls at the point of data fetching:

const res = await fetch('https://api.example.com/posts', {
    next: {
        revalidate: 3600,
        tags: ['posts'],
    },
})

When your CMS publishes new content, it calls a webhook. Your route handler receives the webhook, calls revalidateTag('posts'), and Next.js invalidates every cached response that was tagged with posts. The next visitor to any of those pages gets a fresh render. No deploy, no waiting.

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
    const { tag, secret } = await request.json()

    if (secret !== process.env.REVALIDATION_SECRET) {
        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    revalidateTag(tag)
    return NextResponse.json({ revalidated: true })
}

What ISR Requires

ISR needs a running server (or a serverless environment that Next.js can hand off work to). The background regeneration, the cache storage, the webhook handlers: all of these require a runtime. This matters because of what happens with static export.

What You Lose with output: 'export'

If you set output: 'export' in next.config.ts, Next.js builds a fully static site and writes it to out/. There is no server. Everything that runs at request time is gone, which includes ISR.

This is the trade-off this site makes. It runs on Cloudflare Pages as a static export, so there's no ISR. Content updates require a new deploy. For a personal notebook site with infrequent updates, that's fine. For a product blog with a CMS publishing multiple times per day, it would be a problem.

The rule is straightforward: output: 'export' is incompatible with revalidate, revalidateTag, revalidatePath, and any route handler that runs at request time. The build will warn you if you mix them.

Choosing the Right Mode

The choice comes down to update frequency and infrastructure preference:

  • Content that never changes without a code change: fully static. Keep it simple.
  • Content that updates independently of deploys and benefits from CDN caching: ISR. Set a sensible revalidation window and add on-demand revalidation if you need immediate updates.
  • Content that must be unique per request (user-specific data, real-time feeds): fully dynamic. Accept the trade-off.

ISR is the right default for most content sites that need a backend but aren't deploying every time content changes. The caveat is that you need a hosting environment that supports it. Vercel is the obvious choice. Self-hosted Next.js with a Node server works too. Static export hosts like Cloudflare Pages do not.

Pick the model that matches your actual update patterns, not the one that sounds most sophisticated.

Related notes

  • API Caching

    How to use Cache-Control headers and ETags in your REST API to reduce bandwidth, lower server load, ...

  • Cache Invalidation Strategies in Laravel

    Proven strategies for keeping your Laravel cache in sync with your database, covering tag-based inva...

  • Caching in Laravel

    An overview of Laravel's cache system covering cache drivers, storing and retrieving data, cache tag...


Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.