v1.0.0 — install with
, or wire everything with
. Production-validated against
AWS ECS Fargate with multi-instance Redis (24h live-traffic soak and a
live Redis-reboot drill with zero 5xx — see
),
and exercised end-to-end against (both cache interfaces).
MCP server for inspecting and operating @leejpsd/nextjs-cache-handler Redis caches — health, key inspection, tag state, SWR simulation, and dry-run-first invalidation for AI agents.
v0.4 adds: build-output cache seeding (first request after deploy is a
HIT), pub/sub tag propagation (~3ms cross-instance), Redis Cluster
e2e in CI, a CLI (init / doctor / seed), and agent-native assets
(skill, rules, MCP server).
1.0 freezes the public API under semver — the eight entry points, the
CacheHandlerOptions surface, the Redis key shapes and tag-marker format,
and the metric event names. Upgrading from 0.4.x needs no changes.
🤖 For AI agents
Working with Claude Code / Codex / Cursor? Give your agent this URL and it
will install and wire everything (version detection, wrapper files,
next.config patch, verification):
An agent skill with decision tables, invalidation semantics, and a
troubleshooting playbook ships in the package (AGENTS.md,
skills/nextjs-redis-cache/SKILL.md) and via
npx skills add leejpsd/nextjs-cache-handler.
For cache operations from your agent (health, tag state, safe invalidation),
the companion MCP server is on the official registry as
io.github.leejpsd/nextjs-cache-handler-mcp — or one command:
npx nextjs-cache-handler init --yes wires handlers, rules, and .mcp.json together.
The Redis cache handler for Next.js 15/16 that ships bothcacheHandler
(ISR / Pages Router) andcacheHandlers ('use cache' directive,
cacheComponents: true) — the area where
@fortedigital/nextjs-cache-handler
currently lists "Help needed".
That's it. 'use cache', revalidateTag, updateTag, cacheLife all work.
Why this exists
Next.js 16 split caching into two handler interfaces:
Option
Used by
Methods
cacheHandler (singular)
Pages Router ISR, on-demand revalidation
get, set, revalidateTag, resetRequestCache
cacheHandlers (plural)
'use cache' directive, cacheComponents: true
get, set, refreshTags, getExpiration, updateTags
As of 2026-05, the leading OSS Redis handler @fortedigital/[email protected]
declares peerDependencies.next: ">=16.1.5" but the README marks the new
plural interface as ❌ "Not yet supported - Help needed":
📅 Compatibility matrix re-verified 2026-07-31 (from each project's
published README/registry metadata). The OSS Next.js cache
handler ecosystem moves quickly — please verify
@fortedigital
and
nextjs-turbo-redis-cache
directly before relying on this comparison.
Feature
this (0.4.x)
@fortedigital 3.2.1
nextjs-turbo-redis-cache 1.15
cacheHandlers config (plural)
✅
❌ Help needed
✅ since 1.11
'use cache' directive
✅
❌ Help needed
✅ since 1.11
'use cache: remote'
✅ default handler (dedicated multi-tier: roadmap)
❌ Help needed
partial
'use cache: private'
n/a (uncustomizable)
n/a
n/a
cacheComponents: true
✅
❌ Help needed
✅
Build-phase skip (PHASE_PRODUCTION_BUILD)
✅
✅ (singular only)
✅
Auto deploy isolation
✅ BUILD_NAMESPACE env-resolved
manual
✅ BUILD_ID since 1.13
Lua-atomic SET+tag
✅ Lua scripts
partial (MULTI)
partial
AbortSignal timeout
✅ per-op
✅ Proxy-wrapped
❌
Redis Cluster
✅ (cluster adapter, see Production checklist)
✅
✅
ioredis support
✅
✅
✅
In-memory fallback (TTL-aware)
✅
partial
✅ L1 + Redis L2
Next 15 support (ISR handler)
✅ >=15.0.0
✅ (legacy 2.x line)
✅ >=15.0.3
Request-scoped GET dedup
✅
❌
✅
Built-in value compression
✅ gzip/brotli option
example only
example only
Redis Sentinel
✅ (local failover drill)
❌
❌
OpenTelemetry
✅ built-in /otel emitter + onMetric hook
❌
❌
Reconnect strategy
✅ exponential backoff
client-level
error-threshold restart
Live-traffic dogfood (24h+)
✅ AWS ECS Fargate
not published
not published
PR #207 on
@fortedigital (their cacheHandlers attempt) was held up in review over
PHASE_PRODUCTION_BUILD handling — which this package has from the start.
Quick start
Install
npm install @leejpsd/nextjs-cache-handler redis
# or
npm install @leejpsd/nextjs-cache-handler ioredis
redis and ioredis are optional peer dependencies — install whichever
client you use. Both can be present.
Wire up
Two CommonJS wrapper files in your project root (Next.js's
require.resolve pattern doesn't accept ESM directly):
DEPLOYMENT_VERSION env injected at runtime — every entry key is
prefixed with this so old prerender HTML can't bleed across deploys.
For Docker, set ENV DEPLOYMENT_VERSION=... in your runner stage,
not just the builder. (See docs/build-phase.md.)
cacheMaxMemorySize: 0 — turn off Next's local LRU so multi-instance
reads always hit Redis (or the explicit memory fallback).
outputFileTracingRoot pinned — required for output: "standalone"
to avoid static-chunk-404 issues during a deploy.
abortTimeoutMs: 1500 (default) — protects against a stuck Redis
connection from hanging the request thread.
Redis Cluster: set hashTag: true — multi-key Lua scripts
(set-with-tags.lua, revalidate-hard.lua) require all KEYS to land
on the same hash slot. Without hashTag, cluster deployments will hit
CROSSSLOT Keys in request don't hash to the same slot. The flag wraps
the namespace in {} so every key for a given deploy hashes together.
Cluster support is validated by a dedicated e2e suite against a real
3-master cluster (npm run test:cluster, also in CI) — covering the
multi-key Lua scripts, per-master SCAN propagation, and both handlers.
Not yet load-tested at production scale.
Redis maxmemory-policy: allkeys-lru or noeviction — if you need
bounded memory, choose allkeys-lru. Otherwise noeviction keeps
tag indices intact.
TLS — use rediss:// URLs (e.g. ElastiCache in-transit
encryption). The library auto-detects from the URL scheme.
Health check — call your own /api/health that pings Redis
(separate from the handler) so a Redis outage surfaces in your
monitoring without inducing 5xx in user requests.
not yet supported — dedicated adapter on the roadmap
—
DragonflyDB / KeyDB
Redis-protocol compatible — { type: "redis", url } should work
not validated
Single-flight refresh lock (v0.2)
The cacheHandlers (plural) interface returns stale entries inside the
SWR window so users get an instant response while the background
refresh completes. With many instances, the moment an entry crosses the
revalidate boundary, every instance independently triggers its own
refresh — N parallel re-renders for the same key, each hitting your
origin once.
singleFlight: true adds an opt-in Redis lock (refresh-tag-lock.lua,
default TTL 10s) at the SWR boundary. The first instance to acquire it
becomes the leader and runs the refresh; the rest become
followers, keep serving the same stale entry, and wait for the
leader's write to land. The lock is observability-only at the handler
layer — Next.js still drives the actual refresh; we just suppress the
stampede.
this instance just acquired the lock and is the designated refresher
cache.stale.refresh.follower
another instance holds the lock; we serve stale and skip the refresh
If lock acquisition fails (Redis hiccup, TTL race), the handler
defaults to the follower path — the stale entry is always served, never
dropped. This is intentional: the lock is an optimization, not a
correctness-critical primitive.
When not to enable single-flight: small fleets (1–2 instances) where
Next's per-process serialization already covers the stampede risk.
Adding a Redis round-trip per stale read isn't free.
See docs/architecture.md
for the full state machine and a reference to the Lua script body.
OpenTelemetry instrumentation (v0.2)
The handler doesn't bundle @opentelemetry/api (zero runtime
dependencies stays a goal). Instead, the onMetric(event) hook gives
strictly-typed events you can pipe into whatever observability stack
you already run.
exposes a nextjs_cache.events_total counter dimensioned on
type / freshness / backend / reason / op
exposes a nextjs_cache.op_latency_ms histogram for events that
carry an ms field
keeps cardinality bounded — cache keys and tag names are never
emitted as attributes
See examples/opentelemetry/README.md
for setup and three suggested dashboards (hit rate, single-flight
leadership distribution, op latency tails).
How it differs from @fortedigital/nextjs-cache-handler
Three deliberate departures, all rooted in lessons from production
incidents (see docs/):
Build-phase skip is the default, not an opt-in. Every Redis call
goes through a shouldUseRedis() gate that short-circuits when
process.env.NEXT_PHASE === "phase-production-build". PR #207 on
@fortedigital was rejected for missing exactly this.
Deployment isolation is automatic. Every entry key includes
BUILD_NAMESPACE (=process.env.DEPLOYMENT_VERSION) by default. New
deploys can never read entries written by old ones — fixes the
"static chunk 404 after deploy" failure mode without manual cache
flushes.
Lua-atomic tag updates. set writes the entry and updates tag
indices in a single Lua transaction. updateTags(..., {expire: 0})
removes matching entries with one EVALSHA. No window for half-applied
sets to leak dangling tag members.
When @fortedigital ships its cacheHandlers support (PR #207 / feature
branch feature/cache-components), this package will continue to differ
on (2) and (3). For (1), we consider it table-stakes; the upstream's
eventual implementation should converge on the same behavior.
Migrating from another cache handler
Coming from @neshca/cache-handler (or its forks like
@jadkins89/next-cache-handler)? There's a dedicated guide with a full
API/concept mapping, config diff, and a seeding replacement:
docs/migrating-from-neshca.md.
Compatibility
Upgrading to Next.js 16.3? Two behavioral notes: (1) 16.3 changed the
internal fetch-cache key format (v3 → v4), so 16.2-era fetch entries
become unreadable orphans — deploy with a new DEPLOYMENT_VERSION and let
the old namespace age out via TTL. (2) An ISR entry found past its
cacheLifeexpire now triggers a blocking revalidation instead of
serving stale (matching 'use cache' semantics); within
revalidate..expire stale-while-revalidate is unchanged.
Next.js: >=15.0.0 <17
cacheHandler (singular, ISR): Next 15 and 16 — the handler accepts
both ctx shapes (Next 15's ctx.revalidate / kindHint, Next 16's
ctx.cacheControl / kind)
cacheHandlers (plural, 'use cache'): Next >=16.1.5 only — the
interface does not exist before 16
Node.js: >=20
redis: >=5.0.0 (peer, optional)
ioredis: >=5.0.0 (peer, optional) — also powers type: "cluster" and
type: "sentinel" (Sentinel master discovery with automatic failover)
ESM and CJS dual-published, full TypeScript types, validated via
arethetypeswrong and
publint.
0.4.2 — upstream-exact tag invalidation semantics: no-durations
updateTags is hard (updateTag() read-your-own-writes), profile
durations are soft with a real hard deadline; tag-stale entries are
served with revalidate: -1 instead of a backdated timestamp ✅
v1.0.0(2026-08) — public API frozen under semver; every export
documented in docs/api.md ✅
Next — Vercel KV / Upstash adapters, 'use cache: remote' multi-tier
setup, neshClassicCache equivalent. All additive; no 2.0 needed.
MCP server for inspecting and operating @leejpsd/nextjs-cache-handler Redis caches — health, key inspection, tag state, SWR simulation, and dry-run-first invalidation for AI agents.
ตรวจสอบล่าสุดเมื่อ 2026-11-14 MCP server for inspecting and operating @leejpsd/nextjs-cache-handler Redis caches — health, key inspection, tag state, SWR simulation, and dry-run-first invalidation for AI agents.