npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@trieb.work/nextjs-turbo-redis-cache

v1.18.0

Published

Designed for speed, scalability, and optimized performance, nextjs-turbo-redis-cache is your custom cache handler for demanding production environments.

Readme

nextjs-turbo-redis-cache - Next.js Cache Handler

npm version

The ultimate Redis Cache Handler for Next.js 15 / 16, supporting both the App Router and the Pages Router. Built for production-ready, large-scale projects, it delivers unparalleled performance and efficiency with features tailored for high-traffic applications. This package has been created after extensibly testing the @neshca package and finding several major issues with it.

Key Features:

  • Batch Tag Invalidation: Groups and optimizes delete operations for minimal Redis stress.
  • Request Deduplication: Prevents redundant Redis get calls, ensuring faster response times.
  • In-Memory Caching: Includes local caching for Redis get operations to reduce latency further. Don't request redis for the same key multiple times.
  • Efficient Tag Management: in-memory tags map for lightning-fast revalidate operations with minimal Redis overhead.
  • Intelligent Key-Space Notifications: Automatic update of in-memory tags map for expired or evicted keys.

This library offers you an easy and high performant caching solution for docker, Kubernetes or Google Cloud Run deployments of Next.js. Read more on how it originated at TRWK> Case Study.

For a deep dive into the internal architecture (shared hash maps, SyncedMap, request deduplication, and how get/set/revalidateTag work under the hood), see ARCHITECTURE.md.

Compatibility

This package is compatible with Next.js 15.0.3 and above, with the App Router, the Pages Router, or both together in a hybrid app. It is not compatible with Next.js 14.x. or 15-canary. Redis Server need to have Redis Server Version 2.8.0 or higher and have to be configured with notify-keyspace-events to be able to use the key-space notifications feature.

Pages Router support covers ISR pages (getStaticProps + revalidate, getStaticPaths with fallback), notFound: true and redirect: results, and on-demand revalidation via res.revalidate(path) — including across multiple server instances sharing one Redis (see the two-instance integration test). App Router and Pages Router entries are handled side by side by the same handler instance, so hybrid apps that use both routers work without any extra configuration.

Tested versions are:

  • Nextjs 15.0.3 + redis client 4.7.0
  • Nextjs 15.2.4 + redis client 4.7.0
  • Nextjs 15.3.2 + redis client 4.7.0
  • Nextjs 15.4.11 + redis client 4.7.0
  • Nextjs 16.0.11 + redis client 4.7.0 (cacheComponents: false)
  • Nextjs 16.2.6 + redis client 4.7.0 (cacheComponents: false)
  • Nextjs 16.2.6 + redis client 4.7.0 (cacheComponents: true)
  • Nextjs 16.2.6 + redis client 4.7.0 (Pages Router)
  • Nextjs 16.3.0 + redis client 4.7.0 (cacheComponents: false)
  • Nextjs 16.3.0 + redis client 4.7.0 (cacheComponents: true)
  • Nextjs 16.3.0 + redis client 4.7.0 (Pages Router)

Cache Components (Next.js 16+) are fully supported. Automated test coverage includes 'use cache', cacheTag, and cacheLife flows in the Cache Components integration suite.

For Cache Components, see the "Cache Components handler (Next.js 16+)" section below.

Getting started

Enable redis key-space notifications for Expire and Evict events

redis-cli -h localhost config set notify-keyspace-events Exe

Install package

pnpm install @trieb.work/nextjs-turbo-redis-cache

Setup environment variables in your project/deployment

REDISHOST and REDISPORT environment variables are required. KEY_PREFIX, VERCEL_URL, VERCEL_ENV are optional. For the bundled Next.js handlers (default cache handler and Cache Components handler), the key prefix precedence is:

  • options.keyPrefix → KEY*PREFIX → VERCEL_URL → BUILD_ID (from .next/BUILD_ID) → UNDEFINED_URL*

For direct usage of RedisStringsHandler, the default remains framework-agnostic:

  • options.keyPrefix → KEY*PREFIX → VERCEL_URL → UNDEFINED_URL*

VERCEL_ENV is used to determine the database to use. Only VERCEL_ENV=production will show up in DB 0 (redis default db). All other values of VERCEL_ENV will use DB 1, use redis-cli -n 1 to connect to different DB 1. This is another protection feature to avoid that different environments (e.g. staging and production) will overwrite each other. Furthermore there exists the DEBUG_CACHE_HANDLER environment variable to enable debug logging of the caching handler once it is set to true.

There exists also the SKIP_KEYSPACE_CONFIG_CHECK environment variable to skip the check for the keyspace configuration. This is useful if you are using redis in a cloud environment that forbids access to config commands. If you set SKIP_KEYSPACE_CONFIG_CHECK=true the check will be skipped and the keyspace configuration will be assumed to be correct (e.g. notify-keyspace-events Exe).

KILL_CONTAINER_ON_ERROR_THRESHOLD: Optional environment variable that defines how many Redis client errors should occur before the process exits with code 1. This is useful in container environments like Kubernetes where you want the container to restart if Redis connectivity issues persist. Set to 0 (default) to disable this feature. For example, setting KILL_CONTAINER_ON_ERROR_THRESHOLD=10 will exit the process after 10 Redis client errors, allowing the container orchestrator to restart the container.

REDIS_COMMAND_TIMEOUT_MS: Optional environment variable that sets the timeout in milliseconds for Redis get command. If not set, defaults to 500ms. The value is parsed as an integer, and if parsing fails, falls back to the 500ms default.

Option A: minimum implementation with default options

extend next.config.js with:

const nextConfig = {
  ...
  cacheHandler: require.resolve("@trieb.work/nextjs-turbo-redis-cache")
  ...
}

Make sure to set either REDIS_URL or REDISHOST and REDISPORT environment variables.

Redis connections are skipped during next build (NEXT_PHASE=phase-production-build), so a production build can succeed without Redis. The handler connects when Next.js first calls it at runtime (next start).

Option B: create a wrapper file to change options

create new file customized-cache-handler.js in your project root and add the following code:

const { RedisStringsHandler } = require('@trieb.work/nextjs-turbo-redis-cache');

let cachedHandler;

module.exports = class CustomizedCacheHandler {
  constructor() {
    if (!cachedHandler) {
      cachedHandler = new RedisStringsHandler({
        database: 0,
        keyPrefix: 'test',
        timeoutMs: 2_000,
        revalidateTagQuerySize: 500,
        sharedTagsKey: '__sharedTags__',
        avgResyncIntervalMs: 10_000 * 60,
        redisGetDeduplication: false,
        inMemoryCachingTime: 0,
        defaultStaleAge: 1209600,
        estimateExpireAge: (staleAge) => staleAge * 2,
      });
    }
  }
  get(...args) {
    return cachedHandler.get(...args);
  }
  set(...args) {
    return cachedHandler.set(...args);
  }
  revalidateTag(...args) {
    return cachedHandler.revalidateTag(...args);
  }
  resetRequestCache(...args) {
    return cachedHandler.resetRequestCache(...args);
  }
}

defaultStaleAge and estimateExpireAge are fallbacks for when Next.js does not pass cacheControl.expire. On Next.js 16.3+ ISR, Redis TTL is expire and these options do not change it.

extend next.config.js with:

const nextConfig = {
  ...
  cacheHandler: require.resolve("./customized-cache-handler")
  ...
}

A working example of above can be found in the test/nextjs-test-projects/next-app-customized folder.

Available Options

| Option | Description | Default Value | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | redisUrl | Redis connection url | process.env.REDIS_URL? process.env.REDIS_URL : process.env.REDISHOST ? redis://${process.env.REDISHOST}:${process.env.REDISPORT} : 'redis://localhost:6379' | | database | Redis database number to use. Uses DB 0 for production, DB 1 otherwise | process.env.VERCEL_ENV === 'production' ? 0 : 1 | | keyPrefix | Prefix added to all Redis keys | RedisStringsHandler default: process.env.KEY_PREFIX \|\| process.env.VERCEL_URL \|\| 'UNDEFINED_URL_' Next handlers resolve: options.keyPrefix \|\| KEY_PREFIX \|\| VERCEL_URL \|\| BUILD_ID \|\| 'UNDEFINED_URL_' | | sharedTagsKey | Key used to store shared tags hash map in Redis | '__sharedTags__' | | getTimeoutMs | Timeout in milliseconds for time critical Redis operations. If Redis get is not fulfilled within this time, returns null to avoid blocking site rendering. | process.env.REDIS_COMMAND_TIMEOUT_MS ? (Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 500) : 500 | | revalidateTagQuerySize | Number of entries to query in one batch during full sync of shared tags hash map | 250 | | avgResyncIntervalMs | Average interval in milliseconds between tag map full re-syncs | 3600000 (1 hour) | | redisGetDeduplication | Enable deduplication of Redis get requests via internal in-memory cache. | true | | inMemoryCachingTime | Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely. | 10000 | | defaultStaleAge | Fallback stale age in seconds used only when Next.js does not pass a finite cacheControl.expire (e.g. revalidate: false, or older callers that only send revalidate). Next 16.3+ ISR typically sends expire (~1 year); that value is the Redis TTL and this option is ignored. | 1209600 (14 days) | | estimateExpireAge | Fallback to compute Redis TTL from a stale/revalidate age when cacheControl.expire is absent. Not applied when Next.js provides expire. | Production: staleAge * 2 Other: staleAge * 1.2 | | socketOptions | Redis client socket options for TLS/SSL configuration (e.g., { tls: true, rejectUnauthorized: false }) | { connectTimeout: timeoutMs } | | clientOptions | Additional Redis client options (e.g., username, password) | undefined | | killContainerOnErrorThreshold | Number of consecutive errors before the container is killed. Set to 0 to disable. | Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0 | | valueSerializer | Pluggable wire-format codec for Redis string values (compression, encryption, custom encoding). See Custom value serializer. | jsonCacheValueSerializer (JSON.stringify with built-in Buffer and Map encoding) |

Custom value serializer (compression, encryption)

By default cache entries are stored as JSON.stringify(...) with built-in Buffer and Map encoding. For workloads where the encoded payload is large (big RSC trees, large fetch responses) or sensitive (PII), you can plug in a custom codec - gzip, brotli, AES, anything - via the valueSerializer option, without forking this package or losing the existing dedup / batch / keyspace features.

Contract

  • serialize(entry) is called on every set() with the in-memory CacheEntry. It must return the string written to Redis, or a Promise<string> for async codecs.
  • deserialize(stored) is called on every cache hit with the exact string read from Redis. It must return a CacheEntry, null, or a Promise of either. Returning null is treated as a cache miss - the handler returns null from get() without surfacing an error.
  • Both methods may be async, enabling non-blocking codecs such as zlib.brotliCompress or crypto.subtle that don't block the Node.js event loop. Synchronous implementations continue to work unchanged.
  • Only the main cache-entry storage path is routed through the serializer. Internal structures (__sharedTags__, __revalidated_tags__, inMemoryDeduplicationCache) are not affected.

Default export for reuse

The default serializer is exported so you can wrap it (e.g. compress + JSON fallback) or compare against it by reference to detect that no custom serializer was configured. The underlying Buffer / Map JSON helpers used by the default are also exported for use inside custom codecs:

import {
  jsonCacheValueSerializer,
  bufferAndMapReplacer,
  bufferAndMapReviver,
} from '@trieb.work/nextjs-turbo-redis-cache';

Important: a plain JSON.stringify(value) does not preserve native Buffer or Map values inside a cache entry. RSC payloads contain Buffers. If you write a custom codec that doesn't use the exported bufferAndMapReplacer / bufferAndMapReviver (or doesn't wrap jsonCacheValueSerializer), expect those to come back as plain objects.

Example: gzip (sync)

Wraps bufferAndMapReplacer / bufferAndMapReviver so native Buffer and Map values inside the cache entry round-trip unchanged. gzipSync / gunzipSync block the event loop - prefer the async brotli example below for hot workloads.

import { gzipSync, gunzipSync } from 'node:zlib';
import {
  RedisStringsHandler,
  bufferAndMapReplacer,
  bufferAndMapReviver,
} from '@trieb.work/nextjs-turbo-redis-cache';

const gzipSerializer = {
  serialize(value) {
    const json = JSON.stringify(value, bufferAndMapReplacer);
    return gzipSync(json).toString('base64');
  },
  deserialize(stored) {
    const buf = Buffer.from(stored, 'base64');
    return JSON.parse(gunzipSync(buf).toString('utf8'), bufferAndMapReviver);
  },
};

export default class CustomizedCacheHandler {
  constructor() {
    this.handler = new RedisStringsHandler({
      valueSerializer: gzipSerializer,
    });
  }
  // ... delegate get/set/revalidateTag/resetRequestCache to this.handler
}

Example: brotli (async, non-blocking)

Uses promisify(brotliCompress) and promisify(brotliDecompress) so compression runs on a worker thread and doesn't block the event loop.

import { promisify } from 'node:util';
import { brotliCompress, brotliDecompress } from 'node:zlib';
import {
  RedisStringsHandler,
  bufferAndMapReplacer,
  bufferAndMapReviver,
} from '@trieb.work/nextjs-turbo-redis-cache';

const brotliCompressAsync = promisify(brotliCompress);
const brotliDecompressAsync = promisify(brotliDecompress);

const brotliSerializer = {
  async serialize(value) {
    const json = JSON.stringify(value, bufferAndMapReplacer);
    const compressed = await brotliCompressAsync(Buffer.from(json, 'utf8'));
    return compressed.toString('base64');
  },
  async deserialize(stored) {
    const buf = Buffer.from(stored, 'base64');
    const decompressed = await brotliDecompressAsync(buf);
    return JSON.parse(decompressed.toString('utf8'), bufferAndMapReviver);
  },
};

export default class CustomizedCacheHandler {
  constructor() {
    this.handler = new RedisStringsHandler({
      valueSerializer: brotliSerializer,
    });
  }
  // ... delegate get/set/revalidateTag/resetRequestCache to this.handler
}

Operational notes

  • Changing the serializer makes existing Redis keys unreadable. Any change to the codec - swapping JSON for gzip, bumping a compression level, rotating an encryption key - means previously written entries can no longer be decoded. Either flush the affected keys (FLUSHDB, or scoped UNLINK of keyPrefix*) or bump keyPrefix before deploying so old and new entries live in disjoint keyspaces.
  • Buffer and Map encoding is built into the default. The default jsonCacheValueSerializer uses this package's bufferAndMapReplacer / bufferAndMapReviver so native Buffer and Map values inside a CacheEntry round-trip transparently. If you write a custom serializer that doesn't reuse those (e.g. plain JSON.stringify over a binary payload), expect RSC payload Buffers to come back as plain { type: 'Buffer', data: [...] } objects. Reuse the exported default inside your codec, or use the exported bufferAndMapReplacer / bufferAndMapReviver, to keep that behavior.
  • The in-memory deduplication cache stores the wire-format string verbatim. When redisGetDeduplication is enabled (default), the value seeded after set() and returned to subsequent get() calls is the exact string produced by serialize(). With a compressing or encrypting codec that means every dedup hit re-runs deserialize() (i.e. re-decompresses or re-decrypts). For very hot keys, evaluate whether the per-hit codec cost outweighs the Redis round-trip the dedup is saving.
  • Other internal trieb structures are not affected by valueSerializer. Only the main cache entries written by set() and read by get() go through the codec. The shared-tags map and the revalidated-tags map are untouched.
  • Cache Components handler is out of scope for now. This option only affects RedisStringsHandler. The Next.js 16+ CacheComponentsHandler does not currently route through valueSerializer; that's a candidate follow-up.

TLS Configuration

To connect to Redis using TLS/SSL (e.g., when using Redis over rediss:// URLs), you can configure the socket options. Here's an example:

const { RedisStringsHandler } = require('@trieb.work/nextjs-turbo-redis-cache');

let cachedHandler;

module.exports = class CustomizedCacheHandler {
  constructor() {
    if (!cachedHandler) {
      cachedHandler = new RedisStringsHandler({
        redisUrl: 'rediss://your-redis-host:6380', // Note the rediss:// protocol
        socketOptions: {
          tls: true,
          rejectUnauthorized: false, // Only use this if you want to skip certificate validation
        },
      });
    }
  }
  // ... rest of the handler implementation
};

Consistency of Redis and this caching implementation

For a detailed description of the shared hash maps (sharedTagsMap, revalidatedTagsMap), the SyncedMap Pub/Sub mechanism, and the DeduplicatedRequestHandler, see ARCHITECTURE.md.

To understand consistency levels of this caching implementation we first have to understand the consistency of redis itself: Redis executes commands in a single-threaded manner. This ensures that all operations are processed sequentially, so clients always see a consistent view of the data. But depending on the setup of redis this can change:

  • Strong consistency: only for single node setup
  • Eventual consistency: In a master-replica setup (strong consistency only while there is no failover)
  • Eventual consistency: In Redis Cluster mode

Consistency levels of the Caching Handler

Strong consistency is only achievable when all of the following conditions are met:

  1. Redis is used in a single node setup
  2. Only a single application instance is running (no cross-instance Pub/Sub propagation delay)
  3. Request Deduplication is turned off (redisGetDeduplication: false)

In practice, most deployments run multiple application instances, which introduces eventual consistency through two mechanisms:

Source 1: SyncedMap Pub/Sub propagation (both handlers)

Both RedisStringsHandler and CacheComponentsHandler maintain in-memory maps (sharedTagsMap, revalidatedTagsMap) that are synchronized across instances via Redis Pub/Sub (see SyncedMap in ARCHITECTURE.md). When a tag is revalidated on instance 1, the Pub/Sub message must propagate to instance 2 before its local maps reflect the change. Until the message arrives, get() on instance 2 may still consider a cache entry valid.

This is especially relevant for implicit tags (_N_T_ prefix): when revalidatePath("/products") is called, the handler records a timestamp in revalidatedTagsMap and lazily invalidates nested fetch entries on the next get(). If another instance hasn't received the Pub/Sub update yet, it can serve a stale fetch result.

The propagation delay is typically in the order of a few milliseconds (Redis Pub/Sub round-trip).

Source 2: Request Deduplication (both handlers)

Both RedisStringsHandler and CacheComponentsHandler use DeduplicatedRequestHandler (enabled by default via redisGetDeduplication: true). It caches Redis GET results in memory for inMemoryCachingTime (default 10s). The following sequence can occur across instances:

Instance 1: call set A 1
Instance 1: served set A 1
Instance 2: call get A           → result cached in dedup cache
Instance 1: call delete A        → revalidateTag deletes from Redis + publishes Pub/Sub delete
Instance 2: call get A           → served from dedup cache (stale!) if Pub/Sub delete hasn't arrived
Instance 2: served get A → 1     (should already be deleted)
Instance 1: served delete A

When revalidateTag / updateTags runs, it also deletes the affected keys from the inMemoryDeduplicationCache and broadcasts this deletion via Pub/Sub. So the consistency window is bounded by the Pub/Sub propagation time (typically 5–100ms), not the full inMemoryCachingTime. Only if the Pub/Sub message is lost would the stale entry persist for the full caching duration.

Practical impact

Since all caching calls within one API/page/server action request are always served by the same instance, this problem will not occur inside a single request but rather in a combination of multiple parallel requests across instances. The probability that this will affect a single user during a request sequence is very low, since typically a single user will not make a follow-up request during this small time window of typically <100ms. To further mitigate the problem and increase performance (increase local in-memory cache hit ratio) make sure that your load balancer will always serve one user to the same instance (sticky sessions).

By accepting and tolerating this eventual consistency, the performance of the caching handler is significantly increased.

Development

  1. Run pnpm install to install the dependencies
  2. Run pnpm build to build the project
  3. Run pnpm lint to lint the project
  4. Run pnpm format to format the project
  5. Run pnpm run-dev-server to test and develop the caching handler using the nextjs integration test project
  6. If you make changes to the cache handler, you need to stop pnpm run-dev-server and run it again.

Testing

To run all tests you can use the following command:

pnpm build && pnpm test

For CI, we use dedicated scripts:

pnpm test:ci
pnpm test:integration:build-id-prefix

Folder layout / runners:

  • Vitest unit tests live in test/vitest/unit/**; integration tests in test/vitest/integration/**.
  • Playwright (E2E) lives in test/playwright/** (see playwright.config.ts).
  • Test fixtures (Next.js apps) live in test/nextjs-test-projects/.

Unit tests

To run unit tests you can use the following command:

pnpm build && pnpm test:unit

Integration tests

To run integration tests you can use the following command:

pnpm build && pnpm test:integration

To run the BUILD_ID integration test independently:

pnpm build && pnpm test:integration:build-id-prefix

The integration tests will start a Next.js server and test the caching handler. You can modify testing behavior by setting the following environment variables:

  • SKIP_BUILD: If set to true, the integration tests will not build the Next.js app. Therefore the nextjs app needs to be built before running the tests. Or you execute the test once without skip build and the re-execute pnpm test:integration with skip build set to true.
  • SKIP_OPTIONAL_LONG_RUNNER_TESTS: If set to true, the integration tests will not run the optional long runner tests.
  • DEBUG_INTEGRATION: If set to true, the integration tests will print debug information of the test itself to the console.

Integration tests may have dependencies between test cases, so individual test failures should be evaluated in the context of the full test suite rather than in isolation.

E2E tests (Playwright)

To run Playwright tests (test/playwright/**) you can use:

pnpm test:e2e

Cache Components handler (Next.js 16+)

This package can be used as a Cache Components handler (Node.js runtime) for Next.js apps that enable Cache Components.

This is experimental support and the Next.js Cache Components APIs may still change. We don't have a larger production project right now available to test this in real world conditions.

Enable Cache Components + cache handler

Install the package in your Next.js app:

pnpm add @trieb.work/nextjs-turbo-redis-cache redis

Hybrid setup (ISR + Cache Components)

Next.js has two different handler APIs. They are not interchangeable:

| Config key | Next.js loads it as | This package export | Methods | | ------------------------- | --------------------------------- | ------------------------------------------ | ---------------------------------------------------------- | | cacheHandler (singular) | new Handler(options) | default export (CachedHandler class) | get, set, revalidateTag, resetRequestCache | | cacheHandlers (plural) | imported object (not constructed) | redisCacheHandler | get, set, getExpiration, updateTags, refreshTags |

redisCacheHandler is not a constructor (new redisCacheHandler() throws). Pointing cacheHandler at ./cache-handler.js (the Cache Components object) will fail at runtime. Pointing cacheHandlers at the default class export will not provide getExpiration / updateTags.

For a self-hosted app that needs both ISR and 'use cache' / 'use cache: remote':

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheHandler: require.resolve('@trieb.work/nextjs-turbo-redis-cache'),
  cacheHandlers: {
    default: require.resolve('./cache-handler.js'),
    remote: require.resolve('./cache-handler.js'),
  },
  cacheMaxMemorySize: 0,
};

export default nextConfig;
// cache-handler.js — Cache Components only (`cacheHandlers.default` / `.remote`)
const { redisCacheHandler } = require('@trieb.work/nextjs-turbo-redis-cache');

module.exports = redisCacheHandler;

default and remote may be the same redisCacheHandler module: 'use cache' uses default, 'use cache: remote' uses remote. cacheMaxMemorySize: 0 disables Next's in-process memory cache so Redis is shared across instances.

Do not wrap ISR and Cache Components in one file unless you implement both interfaces (class constructed with new, and a separate object export). This package ships them as two exports on purpose.

Cache Components only

If you only need Cache Components (no ISR cacheHandler), enable Cache Components and point cacheHandlers at redisCacheHandler. Include remote if you use 'use cache: remote'.

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheHandlers: {
    default: require.resolve('./cache-handler.js'),
    remote: require.resolve('./cache-handler.js'),
  },
  cacheMaxMemorySize: 0,
};

export default nextConfig;
// cache-handler.js
const { redisCacheHandler } = require('@trieb.work/nextjs-turbo-redis-cache');

module.exports = redisCacheHandler;

If you prefer ESM:

// cache-handler.js
import { redisCacheHandler } from '@trieb.work/nextjs-turbo-redis-cache';

export default redisCacheHandler;

Required environment variables

  • REDIS_URL (recommended): e.g. redis://localhost:6379

Optional:

  • VERCEL_URL: used as a key prefix for multi-tenant isolation (also useful in tests). If unset, a default prefix is used.
  • REDIS_COMMAND_TIMEOUT_MS: timeout (ms) for Redis commands used by the handler.

Official caching semantics (Vercel / Next.js self-hosting docs)

This package follows the semantics documented in the Next.js self-hosting guide and the official cache-handler-redis example:

| Topic | Behavior | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ISR Redis TTL | Key TTL on cacheControl.expire, not revalidate. Past revalidate an entry is only stale (SWR); evicting at that boundary would defeat background refresh. Legacy callers that only pass revalidate still get estimateExpireAge(revalidate). | | Tag sources (ISR) | APP_PAGE / APP_ROUTE tags come from data.headers['x-next-cache-tags'] plus ctx.tags. FETCH tags come from ctx.tags. | | Buffer / Map serialization | rscData (Buffer) and segmentData (Map) require custom JSON encoding — use the built-in jsonCacheValueSerializer or wrap it. Plain JSON.stringify causes segmentData.get is not a function on RSC navigation. | | updateTags(tags, durations) | Persists Next.js tag-manifest fields in Redis (stale / expired). No durations or { expire: 0 } hard-expires (expired = now). expire > 0 (including 'max' ~1 year) is the SWR window: stale = now, expired = now + expire * 1000, and get() returns revalidate: -1 until that deadline. | | getExpiration pattern | Returns max tag expired (may be in the future). Next.js uses this for implicit/soft tags. Explicit cacheTag()s are checked in get() via areTagsExpired / areTagsStale. | | Build without Redis | During next build (NEXT_PHASE), Redis connections are skipped so CI/build pipelines without Redis still succeed. |

Lazy initialization

The redisCacheHandler export is lazily initialized — importing the package does not open a Redis connection. The connection is deferred until the first method call on the handler (when Next.js invokes it). This means:

  • Importing the package (e.g. for RedisStringsHandler only) never opens a Redis connection, even if Cache Components is not used.
  • getRedisCacheComponentsHandler(options) can be called with custom options before first use to configure the singleton.
  • If getRedisCacheComponentsHandler(options) is called after the handler has already been used, the options are ignored (the singleton is already constructed).

Local manual testing (Cache Lab)

This repo includes a dedicated Next.js Cache Components integration app with real pages for manual testing.

  1. Start Redis locally.
  2. Install + start the Cache Components test app:
pnpm -C test/nextjs-test-projects/next-app-16-2-6-cache-components install
pnpm -C test/nextjs-test-projects/next-app-16-2-6-cache-components dev

Then open the Cache Lab pages:

  • /cache-lab
  • /cache-lab/use-cache-nondeterministic
  • /cache-lab/cachelife-short
  • /cache-lab/tag-invalidation
  • /cache-lab/stale-while-revalidate
  • /cache-lab/runtime-data-suspense
  • /cache-lab/use-cache-remote
  • /cache-lab/revalidate-durations

To run the Playwright E2E tests against a running dev server:

PLAYWRIGHT_BASE_URL=http://localhost:3101 pnpm test:e2e

Some words on nextjs caching internals

Next.js uses different cache entry kinds. This handler supports APP_PAGE, APP_ROUTE, FETCH, PAGES, and REDIRECT (plus Pages Router notFound stored as a null value).

  • app/<segment>/page.tsxAPP_PAGE
  • app/<segment>/route.ts (and /favicon.ico) → APP_ROUTE
  • fetch() inside App Router → FETCH
  • Pages Router getStaticPropsPAGES / REDIRECT

For details on how these kinds are handled internally (tag maps, deduplication, value transformation), see ARCHITECTURE.md.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Sponsor

This project is created and maintained by the Next.js & Payload CMS agency TRWK>, formerly trieb.work.