cacheHandlers config (plural) ❌ Not yet supported - Help needed
'use cache' directive ❌ Not yet supported - Help needed
'use cache: remote' directive ❌ Not yet supported - Help needed
'use cache: private' directive ❌ Not yet supported - Help needed
cacheComponents ❌ Not yet supported - Help needed
cacheHandlers config (plural) ❌ Not yet supported - Help needed
'use cache' directive ❌ Not yet supported - Help needed
'use cache: remote' directive ❌ Not yet supported - Help needed
'use cache: private' directive ❌ Not yet supported - Help needed
cacheComponents ❌ Not yet supported - Help needed
cacheHandlers config (plural) ❌ Not yet supported - Help needed
'use cache' directive ❌ Not yet supported - Help needed
'use cache: remote' directive ❌ Not yet supported - Help needed
'use cache: private' directive ❌ Not yet supported - Help needed
cacheComponents ❌ Not yet supported - Help needed
// next.config.ts
const nextConfig = { cacheComponents: true, cacheHandler: require.resolve("./cache-incremental.cjs"), cacheHandlers: { default: require.resolve("./cache-components.cjs") },
};
// next.config.ts
const nextConfig = { cacheComponents: true, cacheHandler: require.resolve("./cache-incremental.cjs"), cacheHandlers: { default: require.resolve("./cache-components.cjs") },
};
// next.config.ts
const nextConfig = { cacheComponents: true, cacheHandler: require.resolve("./cache-incremental.cjs"), cacheHandlers: { default: require.resolve("./cache-components.cjs") },
};
// 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, // auto deploy isolation abortTimeoutMs: 1500, staleWhileRevalidate: true, singleFlight: true, // optional, opt-in stampede protection (v0.2)
});
// 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, // auto deploy isolation abortTimeoutMs: 1500, staleWhileRevalidate: true, singleFlight: true, // optional, opt-in stampede protection (v0.2)
});
// 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, // auto deploy isolation abortTimeoutMs: 1500, staleWhileRevalidate: true, singleFlight: true, // optional, opt-in stampede protection (v0.2)
});
// next.config.ts (the buggy version)
const useLibrary = process.env.USE_LIBRARY_HANDLER === "true";
const path = useLibrary ? "./lib-cache-components.cjs" : "./redis-handler.cjs"; const nextConfig = { cacheHandlers: { default: require.resolve(path) }, // ...
};
// next.config.ts (the buggy version)
const useLibrary = process.env.USE_LIBRARY_HANDLER === "true";
const path = useLibrary ? "./lib-cache-components.cjs" : "./redis-handler.cjs"; const nextConfig = { cacheHandlers: { default: require.resolve(path) }, // ...
};
// next.config.ts (the buggy version)
const useLibrary = process.env.USE_LIBRARY_HANDLER === "true";
const path = useLibrary ? "./lib-cache-components.cjs" : "./redis-handler.cjs"; const nextConfig = { cacheHandlers: { default: require.resolve(path) }, // ...
};
build time: process.env.USE_LIBRARY_HANDLER === undefined → useLibrary === false → path = "./redis-handler.cjs" → require.resolve("./redis-handler.cjs") = "/abs/path/redis-handler.cjs" → that absolute path is what Next.js bakes into the server bundle runtime: process.env.USE_LIBRARY_HANDLER === "true" // (irrelevant — already baked) → Next.js loads /abs/path/redis-handler.cjs → the library is NEVER required
build time: process.env.USE_LIBRARY_HANDLER === undefined → useLibrary === false → path = "./redis-handler.cjs" → require.resolve("./redis-handler.cjs") = "/abs/path/redis-handler.cjs" → that absolute path is what Next.js bakes into the server bundle runtime: process.env.USE_LIBRARY_HANDLER === "true" // (irrelevant — already baked) → Next.js loads /abs/path/redis-handler.cjs → the library is NEVER required
build time: process.env.USE_LIBRARY_HANDLER === undefined → useLibrary === false → path = "./redis-handler.cjs" → require.resolve("./redis-handler.cjs") = "/abs/path/redis-handler.cjs" → that absolute path is what Next.js bakes into the server bundle runtime: process.env.USE_LIBRARY_HANDLER === "true" // (irrelevant — already baked) → Next.js loads /abs/path/redis-handler.cjs → the library is NEVER required
// cache-components-router.cjs
"use strict";
const useLibrary = process.env.USE_LIBRARY_HANDLER === "true";
module.exports = useLibrary ? require("./lib-cache-components.cjs") : require("./redis-handler.cjs");
// cache-components-router.cjs
"use strict";
const useLibrary = process.env.USE_LIBRARY_HANDLER === "true";
module.exports = useLibrary ? require("./lib-cache-components.cjs") : require("./redis-handler.cjs");
// cache-components-router.cjs
"use strict";
const useLibrary = process.env.USE_LIBRARY_HANDLER === "true";
module.exports = useLibrary ? require("./lib-cache-components.cjs") : require("./redis-handler.cjs");
// next.config.ts (fixed)
cacheHandlers: { default: require.resolve("./cache-components-router.cjs"),
},
// next.config.ts (fixed)
cacheHandlers: { default: require.resolve("./cache-components-router.cjs"),
},
// next.config.ts (fixed)
cacheHandlers: { default: require.resolve("./cache-components-router.cjs"),
},
outputFileTracingIncludes: { "/**/*": [ "./cache-components-router.cjs", "./incremental-router.cjs", "./lib-cache-components.cjs", "./lib-incremental-cache-handler.cjs", "./redis-handler.cjs", "./incremental-cache-handler.js", "./node_modules/@leejpsd/nextjs-cache-handler/**/*", ],
},
outputFileTracingIncludes: { "/**/*": [ "./cache-components-router.cjs", "./incremental-router.cjs", "./lib-cache-components.cjs", "./lib-incremental-cache-handler.cjs", "./redis-handler.cjs", "./incremental-cache-handler.js", "./node_modules/@leejpsd/nextjs-cache-handler/**/*", ],
},
outputFileTracingIncludes: { "/**/*": [ "./cache-components-router.cjs", "./incremental-router.cjs", "./lib-cache-components.cjs", "./lib-incremental-cache-handler.cjs", "./redis-handler.cjs", "./incremental-cache-handler.js", "./node_modules/@leejpsd/nextjs-cache-handler/**/*", ],
},
$ curl /api/cache-debug | jq '.cacheState'
{ "entryKeys": 2, "tagKeys": 2, "tagExpirationKeys": 1, "incrementalEntryKeys": 9, "incrementalTagKeys": 1, "sample": "next-cache:entry:8d5a4f71c4cc:[\"build-...\"]"
} $ curl /api/health | jq '.checks.redis'
{ "ok": true, "latencyMs": 2, "reason": null
}
$ curl /api/cache-debug | jq '.cacheState'
{ "entryKeys": 2, "tagKeys": 2, "tagExpirationKeys": 1, "incrementalEntryKeys": 9, "incrementalTagKeys": 1, "sample": "next-cache:entry:8d5a4f71c4cc:[\"build-...\"]"
} $ curl /api/health | jq '.checks.redis'
{ "ok": true, "latencyMs": 2, "reason": null
}
$ curl /api/cache-debug | jq '.cacheState'
{ "entryKeys": 2, "tagKeys": 2, "tagExpirationKeys": 1, "incrementalEntryKeys": 9, "incrementalTagKeys": 1, "sample": "next-cache:entry:8d5a4f71c4cc:[\"build-...\"]"
} $ curl /api/health | jq '.checks.redis'
{ "ok": true, "latencyMs": 2, "reason": null
}
-- refresh-tag-lock.lua
if redis.call('GET', KEYS[1]) then return 0 end
redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[2]))
return 1
-- refresh-tag-lock.lua
if redis.call('GET', KEYS[1]) then return 0 end
redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[2]))
return 1
-- refresh-tag-lock.lua
if redis.call('GET', KEYS[1]) then return 0 end
redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[2]))
return 1
# .github/workflows/ci.yml
integration: services: redis: image: redis:7-alpine ports: [6390:6379] steps: - run: npm run test:integration env: INTEGRATION_REDIS_URL: redis://127.0.0.1:6390
# .github/workflows/ci.yml
integration: services: redis: image: redis:7-alpine ports: [6390:6379] steps: - run: npm run test:integration env: INTEGRATION_REDIS_URL: redis://127.0.0.1:6390
# .github/workflows/ci.yml
integration: services: redis: image: redis:7-alpine ports: [6390:6379] steps: - run: npm run test:integration env: INTEGRATION_REDIS_URL: redis://127.0.0.1:6390
npm install @leejpsd/nextjs-cache-handler redis
# or
npm install @leejpsd/nextjs-cache-handler ioredis
npm install @leejpsd/nextjs-cache-handler redis
# or
npm install @leejpsd/nextjs-cache-handler ioredis
npm install @leejpsd/nextjs-cache-handler redis
# or
npm install @leejpsd/nextjs-cache-handler ioredis - cacheHandler (singular) — Pages Router ISR, on-demand revalidation
- cacheHandlers (plural) — the new 'use cache' directive, cacheComponents: true - USE_LIBRARY_HANDLER=true correctly set
- The deploy commit hash showing the latest code
- The library being installed in node_modules
- The next.config.ts toggle logic being correct - Cache key shapes carry the BUILD_NAMESPACE prefix (8d5a4f71c4cc is the deployment SHA). If the in-tree handler were active, keys would be next-cache:entry:["build-..."] with no namespace segment. That single character difference is the deployment-isolation guarantee in action.
- Redis ping at 2ms — well within the 1500ms abortTimeoutMs budget. No timeout events recorded during the validation window. - redis@5 / ioredis method shape changes between minor versions
- Lua EVAL/EVALSHA semantics on a real server
- Cursor-based scanIterator chunk behavior (the redis@4 → redis@5 upgrade silently broke scanning in the reference deployment, surfacing only under live traffic)
- TTL/EX behavior under real Redis time - The dogfood window is a starting point, not a completion criterion. Memory leaks, timer drift, and edge cases that only surface after extended uptime are not yet covered. Patch releases will accumulate live time as the package ages.
- Redis Cluster is implemented but not load-tested at scale. The hashTag: true flag routes multi-key Lua scripts to the same slot, but I haven't run a real-world cluster benchmark. v0.3 milestone.
- Vercel KV / Upstash adapters ship in v0.3. Both work today via the standard redis@5 adapter against their Redis-compatible endpoints, but native adapters with edge-runtime support are scoped for v0.3.
- Provenance attestation ships from the GitHub Actions OIDC publish path (now wired up in release.yml). The first stable was published from a local machine without provenance; v0.2.x tarballs published via the workflow will carry the verified attestation. - Dogfood before promotion. Running the rc against my own production traffic surfaced the build-time-vs-runtime trap before it could embarrass me publicly. The dogfood plan (docs/staging-dogfood.md) is the single most useful piece of project hygiene I added.
- Frozen spec snapshot in repo. I copied Next.js's official cacheHandlers spec verbatim into docs/next16-spec.md before writing any handler code. CI prints its sha256 on every run so spec drift gets noticed before it bites.
- Compatibility matrix with timestamp. Both upstream packages I compare against are evolving. Putting "verified 2026-05-10" on the matrix is the difference between an honest snapshot and a future lie. - Should have started with outputFileTracingIncludes from day one. I burned half a day on the build-phase trap because Next.js's output: standalone quietly strips files that aren't transitively required by the build-time code path. If you're building anything that gets loaded via require.resolve() from next.config.ts, pin it explicitly.
- Should have shipped Redis Cluster load test results before claiming "Redis Cluster ✅" in the matrix. The current matrix line says "unit-tested, not yet load-tested at scale" — honest, but only because I caught the gap during the pre-publish self-audit. Future me writes the load test first. - README compatibility matrix — verify against your environment
- docs/build-phase.md — the build-time vs runtime trap deep dive
- docs/architecture.md — read paths, write paths, single-flight state machine
- docs/staging-dogfood.md — the verification checklist before promoting your own dogfood to stable