Open source · 7 min read

Redis cache handler for self-hosted Next.js 15 and 16: ISR and 'use cache'

@leejpsd/nextjs-cache-handler is a Redis cache handler for self-hosted Next.js. It stores the Next.js cache in Redis so that every instance of your app serves the same pages and sees the same invalidations. It covers Next.js 15 and 16, and implements both handler interfaces: cacheHandler for ISR and the fetch cache, and cacheHandlers for the 'use cache' directive in Next.js 16.

npm install @leejpsd/nextjs-cache-handler redis

It is MIT licensed and runs on Node.js 20 or later. The package has no dependencies of its own. The Redis client is a peer dependency, so install redis or ioredis, whichever you already use.

The problem: each instance keeps its own cache

By default, Next.js keeps its cache on the local disk and in the memory of each server. With one server, that is fine. With multiple instances behind a load balancer, whether they are Docker containers on Kubernetes or AWS ECS or plain servers, you see symptoms like these:

  • A page can look different from one refresh to the next, because each instance rendered and cached it at a different time.
  • revalidateTag() or a revalidation webhook reaches one instance, and the others keep serving the old page.
  • After a deploy, each instance regenerates the same pages on its own.

The Next.js self-hosting guide describes this and points to a custom cache handler as the fix. This package is one such handler, backed by Redis.

cacheHandler vs cacheHandlers

Next.js 15 has one cache handler option. Next.js 16 has two, and an app can use both at once.

OptionUsed bySupported here on
cacheHandler (singular)ISR and prerendered routes, on-demand revalidation, the fetch cacheNext.js 15 and 16
cacheHandlers (plural)the 'use cache' directive with cacheComponents: trueNext.js 16.1.5 or later

A handler written for one does not cover the other. This package provides a handler for each, and both take the same options.

How to set up the Redis cache handler

Next.js 15, or 16 without 'use cache'

One small CommonJS file and two lines in the config.

// cache-incremental.cjs const { createIncrementalCacheHandler } = require("@leejpsd/nextjs-cache-handler/incremental"); module.exports = createIncrementalCacheHandler({ client: { type: "redis", url: process.env.REDIS_URL }, buildNamespace: process.env.DEPLOYMENT_VERSION, });
// next.config.ts const nextConfig = { cacheMaxMemorySize: 0, // turn off the per-instance memory cache in Next.js cacheHandler: require.resolve("./cache-incremental.cjs"), }; export default nextConfig;

Next.js 16 with 'use cache'

Keep the file above and add a second one for the 'use cache' handler.

// cache-components.cjs const { createCacheComponentsHandler } = require("@leejpsd/nextjs-cache-handler/cache-components"); module.exports = createCacheComponentsHandler({ client: { type: "redis", url: process.env.REDIS_URL }, buildNamespace: process.env.DEPLOYMENT_VERSION, });
// next.config.ts const nextConfig = { cacheComponents: true, cacheMaxMemorySize: 0, // turn off the per-instance memory cache in Next.js cacheHandler: require.resolve("./cache-incremental.cjs"), cacheHandlers: { default: require.resolve("./cache-components.cjs") }, }; export default nextConfig;

Or let the CLI detect your Next.js version, write the files, and patch the config:

npx nextjs-cache-handler init --yes

After that, revalidateTag works as it does on a single server, and so do 'use cache', cacheTag, cacheLife and updateTag on Next.js 16, but every instance now shares the results.

What it does

  • Tag invalidation across instances. On Next.js 16, after revalidateTag(tag, 'max') the existing entry is still served while a fresh one is generated in the background, and updateTag(tag) invalidates the entry immediately, so the next read waits for fresh data. On Next.js 15, revalidateTag(tag) invalidates immediately. Invalidations are stored in Redis, so every instance sees them.
  • Atomic writes. In the 'use cache' handler, an entry and its tag index are written by a single Lua script, so a failed write cannot leave a tag pointing to a missing entry.
  • Deploy isolation. Keys are prefixed with a build namespace (buildNamespace, which defaults to the DEPLOYMENT_VERSION environment variable). Give each deploy its own value and it will not read entries written by the previous one.
  • Seeding at deploy time. npx nextjs-cache-handler seed loads the routes that next build prerendered into Redis, so the first request to those routes after a deploy is a cache hit instead of a render.
  • Redis outage fallback. Each Redis operation has a timeout (1.5 s by default). While Redis is unreachable, the handler falls back to its own bounded in-memory store (1,000 entries by default), which is separate from the Next.js memory cache that cacheMaxMemorySize: 0 turns off.
  • No Redis at build time. next build does not need a Redis connection.
  • Clients and topologies. Works with the redis or ioredis client, Redis Cluster, Redis Sentinel, and TLS through rediss:// URLs.
  • Extras. Optional gzip or Brotli compression, an OpenTelemetry emitter, a doctor command that checks the Redis connection and a write/read round trip, and an MCP server that lets a coding agent inspect the cache.

How it has been tested

Rather than call it "production ready", here is what has been tested.

  • CI on every commit: unit tests, integration tests against a real Redis 7, and an end-to-end suite against a three-master Redis Cluster.
  • Manual runs on Next.js 15.5.29, 16.2.4, 16.3.8 and 16.4.6 with a real Redis (November 2026, cacheHandler only): the test app had an App Router page that reads tagged data, a generateStaticParams route and a Pages Router page with getStaticProps. After next build and seed, the first request to each returned x-nextjs-cache: HIT. On Next.js 16, after revalidateTag(tag, 'max'), the next request was STALE and the one after that was HIT. On Next.js 15.5.29, after revalidateTag(tag), the next request was a MISS with fresh data and the one after that was HIT.
  • Redis stopped mid-run on Next.js 16.4.6: pages kept returning 200. An updateTag() call in a Server Action returned a 500, which tells the caller that the invalidation was not saved. revalidateTag(tag, 'max') logged the error on the server, because Next.js runs it after the response has been sent.
  • Staging deployment on AWS ECS Fargate with two instances and ElastiCache (August 2026, Next.js 16.2.3, a pre-release build of 0.3.0, both handlers active): during a three-minute run at about 14 requests per second, rebooting the Redis node produced zero 5xx responses out of 2,550 requests.

Two gaps to know about. Redis Cluster is covered by the test suites but has not been load-tested at scale. The manual runs above cover only cacheHandler; the 'use cache' handler is covered by the automated suites and the staging run.

Limitations

  • It is for self-hosted apps. On Vercel, you do not need it.
  • Upstash should work over the standard Redis protocol, but it has not been tested.

Alternatives: @neshca/cache-handler and others

This area changes quickly, so check each project's current README before choosing.

FAQ

Does Next.js share its cache between instances?

No. By default, each instance keeps its own cache on local disk and in memory, so ISR pages and tag invalidations are not shared. The Next.js self-hosting guide recommends a custom cache handler for this, and the official example uses Redis.

Does it work with Next.js 15?

Yes. The cacheHandler side supports Next.js 15.0 and later, and was run by hand on 15.5.29. This package supports cacheHandlers from Next.js 16.1.5, so on 15 you only need the first setup above.

Can I use 'use cache' with Redis on Next.js 16?

Yes, from Next.js 16.1.5. Point cacheHandlers.default at the 'use cache' handler as shown in the second setup. cacheTag, cacheLife, revalidateTag and updateTag then read and write Redis.

What happens when Redis goes down?

Each Redis operation times out after 1.5 s by default, and the handler falls back to its in-memory store until Redis is reachable again. In the Next.js 16.4.6 run with Redis stopped, pages kept returning 200, and an updateTag() call failed with a 500 instead of silently skipping the invalidation.

Does it work with Redis Cluster, Sentinel or ioredis?

Yes. It works with the redis and ioredis clients, and with Redis Cluster and Redis Sentinel through ioredis. Cluster has an end-to-end suite in CI.

Can I migrate from @neshca/cache-handler?

Yes. @neshca/cache-handler supports Next.js 13 and 14, so projects moving to 15 or 16 need a replacement. The repository has a migration guide that maps the old config to the new one.

Do I need this on Vercel?

No. On Vercel, the platform manages the cache for you. This package is for self-hosted deployments.

Background

The package came out of running Next.js on several servers, where each instance kept its own cache. Along the way I contributed three merged pull requests to vercel/next.js: fixes to the official Redis cache handler example (#98716), a correction to the cache handlers docs (#96517), and additions to the adapter docs (#96536).

Versions 1.0.2 and 1.0.3 fixed bugs that users reported. If something does not behave as described here, the fastest way to get it fixed is to open an issue that includes your Next.js version and handler options.

Install

npm install @leejpsd/nextjs-cache-handler redis npx nextjs-cache-handler init --yes

If it saves you time, a star on GitHub helps other people find it.