Back to Blog

Next.js ISR Not Revalidating on Self-Hosted Docker Deployments

Published: July 29, 2026
Next.js ISR Not Revalidating on Self-Hosted Docker Deployments

Next.js ISR not revalidating on self-hosted Docker shows up as an inconsistency that's genuinely confusing to debug: revalidate is configured correctly, the page eventually does update — sometimes. Refresh a few times in a row, and you might see the new content, then the old content, then the new content again, depending on which container happens to answer the request. The revalidation logic isn't broken. It's running correctly, once, on exactly one container, while the others have no idea anything changed.

Short answer: Next.js's default ISR cache lives on each container's local filesystem, and if you're running multiple replicas behind a load balancer, each one maintains its own separate copy with no built-in way to know when a sibling instance's cache has changed — the fix is a custom cacheHandler in next.config.js pointing at shared external storage, typically Redis, so every instance reads and writes the same cache instead of its own.

Five identical alarm clocks laid out on a table, each one showing a different time, representing multiple Docker replicas each serving a different, independently cached version of the same page

Why ISR Falls Out of Sync Across Multiple Containers

Next.js's own self-hosting documentation is direct about this: ISR uses "the same Next.js server cache" as general response caching, and by default that cache is stored on the local filesystem of each individual server instance. That works fine for a single next start process with persistent disk. The moment you're running multiple containers — the entire point of scaling horizontally — each one has its own disk, its own independent cache, and no awareness that a sibling container's cache was just invalidated by a revalidation request it never saw. A revalidation triggered on container A updates container A's copy. Containers B and C keep serving whatever they last cached, until a request happens to hit them and trigger their own separate revalidation cycle.

(If you've been restarting containers hoping to "reset" the staleness — that clears the symptom for a moment, but the underlying cause is untouched. The moment traffic resumes, each container starts rebuilding its own independent cache again, and the same inconsistency reappears the next time a page revalidates on only one of them.)

The Fix: A Shared Cache Handler

Next.js's own documented pattern replaces the default per-instance filesystem cache with a custom handler pointing at shared storage:

JavaScript
1// next.config.js
2module.exports = {
3  cacheHandler: require.resolve('./cache-handler.js'),
4  cacheMaxMemorySize: 0, // disable the default in-memory cache
5};
JavaScript
1// cache-handler.js — Redis-backed, shared across every container
2const { createClient } = require('redis');
3
4const client = createClient({ url: process.env.REDIS_URL });
5client.connect();
6
7module.exports = class CacheHandler {
8  constructor(options) {
9    this.options = options;
10  }
11
12  async get(key) {
13    const data = await client.get(key);
14    return data ? JSON.parse(data) : null;
15  }
16
17  async set(key, data, ctx) {
18    await client.set(key, JSON.stringify({
19      value: data,
20      lastModified: Date.now(),
21      tags: ctx.tags,
22    }));
23  }
24
25  async revalidateTag(tags) {
26    tags = [tags].flat();
27    const keys = await client.keys('*');
28    for (const key of keys) {
29      const entry = JSON.parse(await client.get(key));
30      if (entry?.tags?.some((tag) => tags.includes(tag))) {
31        await client.del(key);
32      }
33    }
34  }
35
36  resetRequestCache() {}
37};

Every container now reads from and writes to the same Redis instance instead of its own disk. A revalidation triggered anywhere is immediately visible everywhere, which is exactly the consistency multiple replicas actually need. cacheMaxMemorySize: 0 matters just as much as the handler itself — without it, Next.js's default in-memory layer keeps running alongside your shared store, quietly reintroducing per-instance inconsistency on top of the fix you just made.

A close-up of a bicycle wheel with metal spokes radiating from a central hub, representing every container reading from and writing to one shared cache instead of its own separate copy

Verifying the Fix Actually Reaches Every Replica

Confirming this works means checking more than one container, not just refreshing the page a few times and trusting the load balancer routed you somewhere different each time:

Bash
1# Hit a specific container directly, bypassing the load balancer's usual routing,
2# to confirm each replica serves the same (updated) content after revalidation
3curl -H "Host: yourapp.com" http://container-1-ip:3000/blog/post-slug
4curl -H "Host: yourapp.com" http://container-2-ip:3000/blog/post-slug
5curl -H "Host: yourapp.com" http://container-3-ip:3000/blog/post-slug

If any one of them still returns stale content while the others have updated, the cache handler isn't genuinely shared — worth confirming the Redis connection details are identical across every container's environment configuration, since a typo in one container's REDIS_URL produces exactly this partial-consistency symptom.

A person marking a date on a desk calendar next to a laptop, representing the verification step of confirming a revalidation actually reached every running replica, not just the one that happened to answer first

The Opinion Part

Here's the pattern worth naming, because it's the same one behind Node.js cluster mode not sharing state across workers and several other bugs in this genre: any cache, session, or piece of state that defaults to living in a single process's memory or disk works perfectly right up until you run more than one instance of that process, at which point it becomes a source of quiet, hard-to-reproduce inconsistency rather than a hard failure. ISR's filesystem cache is a sensible default for the common case of a single self-hosted instance — it's not a bug that it doesn't automatically share across replicas, it's a default that assumes a deployment shape you've since outgrown. Scaling horizontally means auditing every piece of state your app assumes is local and moving the ones that need to be shared into something actually shared, and ISR's cache is exactly one of those pieces.

Conclusion

If Next.js ISR isn't revalidating consistently on self-hosted Docker, check whether you're running more than one container first — the default filesystem cache is per-instance by design, and multiple replicas each maintaining their own copy is the far more common cause than a bug in your revalidate configuration. Configure a shared cacheHandler pointing at Redis or similar external storage, disable the default in-memory cache alongside it, and verify the fix by checking multiple replicas directly rather than trusting a single refresh.

If shared state across multiple instances is a recurring theme in your architecture beyond just ISR, our guide on horizontally scaling NestJS with Redis covers the same underlying discipline applied to application-level state instead of the framework's own cache.

Point every container at the same cache, and let a page update once, everywhere, instead of one replica at a time as traffic happens to find it.

Frequently Asked Questions

Because the default ISR cache is stored on each container's local filesystem, and if you're running more than one container behind a load balancer, each one has its own independent copy. Revalidating a page updates the cache on whichever container happened to handle that specific request — the other replicas keep serving their own stale copy until they each separately receive a request that triggers their own revalidation.

It resets the symptom temporarily by clearing each container's local cache back to empty, but it doesn't fix the underlying cause — the moment traffic resumes, each container independently rebuilds its own cache again, and the same inconsistency between replicas reappears the next time a page revalidates on only one of them.

It's a class you provide via the cacheHandler option in next.config.js that replaces Next.js's default per-instance filesystem cache with your own storage backend — commonly Redis or a similar shared store. Because every container reads from and writes to the same external store instead of its own disk, a revalidation triggered on one instance is immediately visible to all of them, which is exactly the consistency multiple replicas need.

Yes — setting cacheMaxMemorySize: 0 alongside your custom cacheHandler is necessary, otherwise Next.js keeps using its default in-memory layer as well, which reintroduces per-instance inconsistency on top of whatever shared store you've configured, defeating the purpose of the custom handler.

Trigger a revalidation, then send several requests specifically distributed across each running container (bypassing the load balancer's usual routing if needed, or simply making enough requests that each replica gets hit) and confirm they all return the updated content at roughly the same time. If any replica still serves stale content after the others have updated, the cache handler isn't actually shared correctly.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio