How to Fix: Cache is always stale on startup for dynamic ISR pages

6 min read

Dynamic ISR pages can appear permanently stale after a restart when multiple Next.js instances boot with an empty in-memory state, then all independently evaluate the same route as needing regeneration. In the reproduction, this shows up most clearly when two servers start on different ports and serve the same Incremental Static Regeneration page: the first request after startup keeps behaving as stale instead of converging cleanly to a fresh cached result.

Understanding the Root Cause

This issue happens because dynamic ISR caching depends on more than the generated HTML file. At runtime, Next.js also needs cache metadata that tells each server instance whether a route is fresh, stale, or ready for background regeneration.

When you run multiple app instances after next build, each process starts with its own runtime cache state. If those instances are not sharing a durable cache layer, they can disagree about the status of the same dynamic route. On startup, a page generated through dynamic ISR may be treated as stale immediately because the instance handling the request does not have synchronized regeneration state from the other instance.

In practical terms, the bug is usually triggered by this combination:

  • A page is generated with ISR using a dynamic route.
  • More than one Next.js server handles requests.
  • The servers do not share a common incremental cache.
  • After restart, each instance reconstructs cache state independently.

This is why the behavior is easy to reproduce with two ports. One instance may regenerate or mark metadata one way, while the other still sees the route as stale on its first request. The result is inconsistent startup behavior that looks like the cache is always stale.

Another important detail: ISR was designed to work best with shared persistent storage in distributed deployments. If you scale horizontally without sharing the underlying incremental cache artifacts, startup consistency becomes unreliable for dynamic pages.

Step-by-Step Solution

The most reliable fix is to make sure all Next.js instances use a shared persistent cache strategy instead of relying on isolated local runtime state.

1. Confirm the page is using ISR intentionally

In the Pages Router, this typically means using getStaticProps with revalidate.

export async function getStaticProps({ params }) {
  const data = await fetchData(params.slug)

  return {
    props: { data },
    revalidate: 60,
  }
}

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: 'blocking',
  }
}

In the App Router, the equivalent usually looks like this:

export const revalidate = 60

export default async function Page({ params }) {
  const data = await fetch(`https://example.com/api/${params.slug}`, {
    next: { revalidate: 60 },
  }).then((r) => r.json())

  return <div>{data.title}</div>
}

If your route is truly dynamic and regenerated at runtime, it is especially important that every instance can read the same cache outcome.

2. Avoid multi-instance deployments with isolated local caches

If you are running:

next start -p 3000
next start -p 3001

and both processes use separate local cache state, you have reproduced the exact class of problem described in the issue. For production, do not assume two standalone Node processes will keep ISR metadata synchronized automatically.

3. Use a shared storage layer for incremental cache data

The durable fix is infrastructure-level: place all instances behind a setup where the generated artifacts and incremental cache state are shared. Depending on your deployment platform, that usually means one of the following:

  • Use a hosting platform with built-in support for shared ISR caching.
  • Use a custom incremental cache handler backed by shared storage.
  • Route traffic to a single active instance if shared cache is not available.

If you are self-hosting, the key idea is simple: all app instances must read and write the same cache.

4. If self-hosting, prefer one instance or sticky traffic until shared cache is implemented

If your environment cannot share incremental cache state yet, the safest workaround is to reduce concurrency at the cache layer:

# Safer temporary workaround
next start -p 3000

Or configure your reverse proxy so the same dynamic route consistently lands on the same instance until regeneration behavior is stable.

5. Validate behavior after restart

After applying the deployment fix, test like this:

npm run build
next start -p 3000
# start your second instance only if shared cache is configured

# request the same dynamic route multiple times after startup
# verify that stale/fresh transitions are consistent

What you want to observe:

  • The first request may trigger generation if the route is not built yet.
  • Subsequent requests should reflect the same cache state across instances.
  • A restart should not cause every instance to rediscover the route independently.

6. Consider on-demand revalidation for highly dynamic content

If content freshness is critical and startup races are expensive, use on-demand revalidation rather than relying only on timed ISR windows.

// Example API route concept
export default async function handler(req, res) {
  try {
    await res.revalidate('/posts/example-slug')
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).json({ revalidated: false })
  }
}

This does not replace shared cache requirements in a multi-instance deployment, but it does reduce ambiguity around when regeneration should occur.

Common Edge Cases

  • Fallback behavior confusion: With fallback: 'blocking', the first request can mask cache-generation timing issues because rendering waits for data. The page still depends on synchronized cache metadata afterward.
  • Ephemeral containers: In Docker, serverless containers, or short-lived VMs, local filesystem cache may disappear between restarts. That makes ISR behavior look unstable even if it worked in a single-process local test.
  • Load balancer randomness: If requests bounce between instances, one request may hit a freshly regenerated copy while the next hits an instance that still marks the page stale.
  • Mixed router patterns: Combining older Pages Router ISR assumptions with newer App Router caching patterns can make debugging harder. Verify which cache model your route actually uses.
  • Data source caching: Sometimes the HTML cache is fine, but the underlying fetch call is separately cached or uncached. That can make ISR look broken when the real mismatch is in upstream data caching.
  • Custom reverse proxies: CDN or proxy caching headers may serve an outdated response and hide whether Next.js itself regenerated the page correctly.

FAQ

Why does this happen only after startup?

Because restart is the moment when each Next.js instance rebuilds its runtime understanding of the incremental cache. If that state is not shared durably, dynamic ISR routes can be evaluated inconsistently right after boot.

Is this a bug in my page code or a deployment architecture problem?

Usually it is more of a deployment architecture problem than a route-code bug. Your ISR page can be perfectly valid, but running multiple isolated instances without shared cache storage creates stale-state races.

Can I fix this by increasing the revalidate time?

No. A larger revalidate window may reduce how often regeneration occurs, but it does not solve the real problem: cache state is not shared consistently across instances.

The core takeaway is straightforward: dynamic ISR is not just about generating HTML; it also depends on synchronized cache metadata. If you start multiple Next.js servers and they do not share the same incremental cache, stale-on-startup behavior is a predictable outcome. The fix is to deploy with shared persistent caching, or keep ISR traffic pinned to a single instance until that shared layer exists.

Leave a Reply

Your email address will not be published. Required fields are marked *