SuperAPI AI Directory

nextjs-redis-cache

Deployment & operations

View repository

Updated 10-15 23:58

About this skill

Next.js Redis Cache Handler

@leejpsd/nextjs-cache-handler is a Redis cache handler for self-hosted Next.js 15/16. It is the only Redis-backed package shipping BOTH handler interfaces: cacheHandler (ISR / Pages Router / fetch cache) and cacheHandlers ('use cache', cacheComponents: true).

Do NOT use it on Vercel-hosted apps — Vercel provides its own managed cache. The target is multi-instance self-hosting: ECS/Fargate, Kubernetes, VMs behind a load balancer.

Version rule: always install >= 0.3.3. Earlier 0.3.x have an incomplete soft-invalidation path (issue #1).

1. Which handler(s) — decision table

Next.js version cacheHandler (ISR) cacheHandlers ('use cache')
15.x ✅ use it ❌ does not exist in Next 15
>= 16.1.5 ✅ use it ✅ use it (cacheComponents: true)
< 15 ❌ unsupported (peer next >=15.0.0 <17) ❌

Detect the version from the app's package.json (dependencies.next). Install: npm i @leejpsd/nextjs-cache-handler plus ONE client: redis (node-redis v5) or ioredis (required for Cluster/Sentinel).

2. Wiring recipe (exact files)

Create TWO CommonJS wrapper files in the project root (Next's require.resolve needs CJS):

// cache-components.cjs  (Next 16 only)
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,
});
// cache-incremental.cjs  (Next 15 and 16)
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 — the four load-bearing keys
const nextConfig = {
  cacheComponents: true,                                   // Next 16 only
  cacheHandler: require.resolve("./cache-incremental.cjs"),
  cacheHandlers: { default: require.resolve("./cache-components.cjs") }, // Next 16 only
  cacheMaxMemorySize: 0,  // REQUIRED: disable Next's local LRU so multi-instance reads hit Redis
};

Env vars: REDIS_URL (rediss:// enables TLS on the redis client type) and DEPLOYMENT_VERSION (per-deploy namespace — git SHA or release id). In Docker, DEPLOYMENT_VERSION MUST be set in the runner stage, not only the builder.

3. Client config selection

{ type: "redis",    url }                      // node-redis v5 — default choice
{ type: "ioredis",  url }                      // ioredis
{ type: "cluster",  nodes: [{host,port}] }     // ioredis Cluster — MUST also set hashTag: true
{ type: "sentinel", sentinels: [{host,port}], name: "mymaster" } // auto master failover
  • Redis Cluster without hashTag: true fails with CROSSSLOT Keys in request don't hash to the same slot (multi-key Lua scripts).
  • A factory function returning any RedisClientLike is accepted for custom clients (Upstash etc. work as generic rediss://).

4. Invalidation semantics — the #1 source of confusion

Call Effect Mechanism
revalidateTag(tag) / revalidateTag(tag, "max") SOFT / SWR: keep serving stale instantly, re-render in background, all instances converge (~1-2s) marker write; read path backdates entries
updateTag(tag) (server action) / hard expire HARD: entries deleted now, next read blocks and regenerates Lua atomic delete
revalidatePath(path) implicit route tags (_N_T_/...) flow through the soft path; Next additionally hard-discards via getExpiration

Facts that prevent misdiagnosis:

  • Soft invalidation is NOT lost: it serves STALE content briefly by design. If content "never updates", check version >= 0.3.3 first.
  • The instance that fires revalidateTag refreshes eagerly (read-your-own-writes). Other instances converge on their next request.
  • Explicit cacheTag() tags are checked by the HANDLER, not by Next — cross-request soft invalidation is entirely the handler's job (verified against next@16.2.3 source).

5. Options quick reference

Option Default When to change
compression: "gzip"|"brotli" off Large RSC payloads; safe to enable on a live cache (reads auto-detect)
memoryMaxEntries 1000 LRU cap of the in-memory outage fallback
fallback: "auto"|"always"|"never" auto never = strict (miss on Redis failure); always = no Redis (build/dev)
singleFlight: true off Many instances + observed duplicate origin work at revalidate boundaries
abortTimeoutMs 1500 Per-op Redis deadline; timeouts degrade to miss
hashTag: true off REQUIRED on Redis Cluster
onMetric — Wire createOtelMetricEmitter() from @leejpsd/nextjs-cache-handler/otel
tagPubSub: true off 0.4+: push-based cross-instance invalidation (~3ms vs seconds); polling stays as safety net; not on Cluster

Reliability built in (0.3+): reconnect with exponential backoff (1s→30s cap), bounded memory fallback, per-op timeouts. A Redis outage degrades to in-memory serving; reconnection is automatic (validated with a live ElastiCache reboot drill: 2550 requests, zero 5xx).

0.4+ deployment accelerators

  • Seed the cache at deploy time so a fresh deployment's first requests are HITs instead of a regeneration stampede: REDIS_URL=... DEPLOYMENT_VERSION=<deploy-id> npx nextjs-cache-handler seed (run after next build, e.g. a Docker entrypoint step; NX semantics — never overwrites live entries).
  • npx nextjs-cache-handler init --yes wires everything above automatically; npx nextjs-cache-handler doctor is the first command to run when debugging connectivity or key-layout issues.
  • MCP server for cache operations from your agent: @leejpsd/nextjs-cache-handler-mcp (cache_health, tag_state, invalidate_tag dry-run, …).

6. Production checklist

  • DEPLOYMENT_VERSION injected at runtime (runner stage in Docker)
  • cacheMaxMemorySize: 0 in next.config
  • output: "standalone" + pinned outputFileTracingRoot
  • Cluster → hashTag: true; managed Redis TLS → rediss://
  • Redis maxmemory-policy: allkeys-lru (bounded) or noeviction
  • Own /api/health endpoint pinging Redis (outages surface in monitoring, not as 5xx)
  • OTel: onMetric: createOtelMetricEmitter() (needs @opentelemetry/api in the app)

7. Troubleshooting playbook (symptom → cause → fix)

Symptom Likely cause Fix
ECONNREFUSED during next build Redis unreachable at build — expected; handler skips Redis in build phase Nothing to fix if build succeeds; keep NEXT_PHASE auto-detection
Page content never updates after revalidateTag(tag, "max") version < 0.3.3 Upgrade
Static chunk 404s right after a deploy DEPLOYMENT_VERSION missing in runner stage → old HTML served from shared Redis Set it; entries are namespaced per deploy
CROSSSLOT errors Redis Cluster without hashTag: true Set the flag
Cache misses on every request across instances cacheMaxMemorySize not 0, or different keyPrefix/namespace per instance Align config; verify with redis-cli KEYS 'next-*'
Works locally, memory-only in prod Redis URL/security group wrong; check logs for redis.connect.failed Connectivity; the app stays up on fallback by design

Key layout for redis-cli debugging: next-cache:entry:<deploy>:<key> · next-cache:tag:<deploy>:<tag> · next-cache:tag-expiration:<deploy>:<tag> · next-incremental:entry:<deploy>:<path> · next-incremental:tag:<tag> (ISR tag states are deliberately NOT namespaced — invalidations survive deploys).

8. Verification recipes

  • ISR round-trip: curl -D- <url> twice → x-nextjs-cache: MISS then HIT; entry visible in redis-cli KEYS 'next-incremental:entry:*'.
  • Soft SWR probe: create a 'use cache' + cacheTag("probe") + cacheLife("hours") page rendering Date.now() with a deliberate ~800ms delay inside the cached fn. Fire revalidateTag("probe", "max") from a route handler. Expect: instant response with the OLD value (stale serve), new value within ~1-2s on all instances. A blocking implementation would eat the 800ms — that's the tell.
  • Multi-instance: run two instances against one Redis; a value cached by instance A must be served by instance B; after invalidation both converge.

9. AWS deployment

The reference topology is ALB → ECS Fargate (2+ tasks) → ElastiCache Redis. Use the AWS agent skills/MCP for the infrastructure itself; use THIS skill for the cache wiring on top. ElastiCache notes: rediss:// for in-transit encryption; single-node is fine to start (reconnect backoff covers node reboots); Sentinel/Cluster supported per §3.

Full API reference: node_modules/@leejpsd/nextjs-cache-handler/README.md and docs/api.md in the repo (https://github.com/leejpsd/nextjs-cache-handler).