Repository navigation
v16.4.3-rc.6 (Release Candidate)
Pre-releaseInstallation
npm install @mrjasonroy/cache-components-cache-handler@16.4.3-rc.6npm dist-tag: rc
Release notes
This is a major release under a patch number. The package version follows Next.js, so the work that went into the 16.4.3 release candidates ships as 16.4.3, and every install on ^16.4.2 or ~16.4.2 gets it on its next update. A 16.4.2 setup keeps working on Node 22.3+ (and, for the page handler, Redis 6.2+ with Lua scripting): deprecated APIs still run, log a one-time warning, and are removed in 16.5.
To upgrade, replace your cache config with withCache and set REDIS_URL in production (or keep your ELASTICACHE_* variables):
// next.config.mjs
import { withCache } from "@mrjasonroy/cache-components-cache-handler";
export default withCache({ cacheComponents: true });To wait, pin the exact version 16.4.2 (not ^16.4.2). docs/migrating.md has the step-by-step guide.
Upgrading from 16.4.2
Before you upgrade
- Node.js 22.3 or later.
engines.nodemoves from>=22.0.0to>=22.3.0. On Node 22.0–22.2 the first cache call throwsCannot load node:…: this package needs Node.js 22.3 or later, and Yarn 1 refuses to install.- Redis 6.2 or later (or Valkey), with Lua scripting allowed, for
withCacheand the page handler. A revalidation runs oneEVALSHAand oneZADD GT; the deprecatedcreateCacheHandlerkeeps its 16.4.2 commands.- Choose a deploy strategy. Revalidations do not cross between 16.4.2 and 16.4.3 instances while both serve during a rolling deploy. Deploy with recreate, or revalidate once from 16.4.3 (for example
revalidatePath("/", "layout")) after the last 16.4.2 instance is gone. Between 16.4.3 builds, revalidations reach every instance.
Behaviour you'll notice
- The page cache starts cold on every deploy. Page and
"use cache: remote"keys are new and scoped by build: the deployment ID whendeploymentIdorNEXT_DEPLOYMENT_IDis set (so skew protection works), elseBUILD_ID; an explicitbuildIdoption wins. A deploy never serves an earlier build's pages, while tag records are shared across builds. 16.4.2's page keys are never read;createCacheHandlerkeeps the 16.4.2 layout and reads its entries ("What's in Redis" in docs/redis.md has the key layout). "use cache"stays in each instance; pages go to Redis. WithwithCache, plain"use cache"values stay in each instance's memory and only"use cache: remote"goes to Redis. If you relied on"use cache"being shared (acreateCacheHandlersetup pointedcacheHandlers.defaultat Redis), switch those functions to"use cache: remote". Pages and route handlers now go to Redis;withCache(config, { pages: false })keeps them out.withCachethrows if the config still setscacheHandlers, orcacheHandlerunlesspages: false.- Revalidations now reach pages in Redis. 16.4.2's page handler never read
x-next-cache-tags, so pages carried no tags andrevalidateTag/revalidatePathnever removed them; it wrote a timestamp and deleted keys only for single-tagfetchentries, and treated a tag array as one tag (["t1","t2"]wrotenextjs:tags:t1,t2and invalidated nothing). 16.4.3 applies every tag of every entry, on every instance. - Pages 16.4.2 never cached are cached now. 16.4.2's
RedisCacheHandlersubtracted milliseconds from seconds when computing a TTL, so it stored nothing with a numericrevalidateor any numericdefaultTTL(its documented3600included). Expect more Redis memory and more hits. - Page entry lifetimes. An entry is kept for
1.5 × revalidate, capped atexpire; without a revalidate period, untilexpire, else fordefaultTTL(default 365 days); never more than 365 days, rounded up to whole seconds (16.4.2: no TTL).RedisCacheHandler.getno longer returnsage; Next computes staleness fromlastModifiedandcacheControl. - A
RedisCacheHandlersubclass with noredisoption readsREDIS_URL,VALKEY_URLor theELASTICACHE_*variables, and runs in memory when none is set. 16.4.2 connected tolocalhost:6379. createCacheHandlerserves stale. It returns an entry until itsexpire, not itsrevalidate, so Next serves it while regenerating in the background (16.4.2 deleted it atrevalidate). Entry TTLs are capped at 366 days, and it adds anEXPIRE(tagTTL, 366 days) on its tag hashes.next buildno longer connects to Redis. Entries written during the build stay in the build process; 16.4.2 wrote them to Redis. The same holds withCACHE_REDIS=off; on the edge runtime the handlers store nothing (see Fixed).- Types.
CacheHandlerConfig.cacheHandleris now optional, so code that annotates a variable asCacheHandlerConfigand usescacheHandleras astringno longer type-checks.createCacheConfig/createCacheConfigWithProfilesstill returncacheHandler: stringwhen given anisrHandlerPath, and emitcacheHandleronly when it is non-empty.
Logs & ops
- Redis commands time out after 1 s (
commandTimeout, default 1000 ms). All handlers in a process share one connection with no offline queue, which reconnects forever with a backoff capped at 5 s. - A Redis outage falls back to memory. Handlers serve from a bounded per-instance store, each entry for at most 60 s, and requests do not fail; that memory is dropped when Redis is back. In 16.4.2 commands queued with no timeout (a
gethung for seconds), and the ElastiCache client stopped retrying after 3 attempts and stayed off until a process restart. - Not for your own client. The timeout, shared connection, outage fallback and build-time behaviour above do not apply to the deprecated
createRedisDataCacheHandlergiven your own client: that client keeps its own settings. - New warnings. Each deprecated API the app calls logs one line per process, naming its replacement and linking the migration guide.
createCacheHandlerwith a Redis backend logs once in production that it does not cover pages, unless aRedisCacheHandleris also in use (suppressIsrWarning: trueturns it off), and aRedisCacheHandlersubclass with no connection setting logs once in production that it runs in memory. - Errors are logged. Redis errors that 16.4.2's
createCacheHandlerswallowed (it logged them only withdebug: true) are now logged, rate-limited (once a minute by default), throughsetCacheLogger's logger or the console. - Old keys stay in Redis. Every page entry 16.4.2 stored (it stored only entries without a TTL), its
nextjs:tags:<tag>strings and itsnextjs:tags:<tag>:keyssets never expire.flushCache({ scope: "all" })deletes everything under the prefix, 16.4.2's keys and the current cache included. docs/migrating.md covers the old keys and docs/operations.md the flush scopes. - In-process memory is capped at 10% of the V8 heap limit by default, shared by every in-process store. Change it with
withCache'smemoryoption orCACHE_MEMORY_HEAP_PERCENT/CACHE_MEMORY_MAX_MIB. - New optional peer dependencies appear in lockfiles:
@aws-crypto/sha256-js,@aws-sdk/credential-provider-nodeand@smithy/signature-v4. They are needed only for ElastiCache IAM authentication and are not installed unless you add them. - Environment variables that 16.4.2 ignored now take effect:
CACHE_REDIS,CACHE_KEY_PREFIX,CACHE_DEBUG,CACHE_COMPRESSION,CACHE_MEMORY_HEAP_PERCENT,CACHE_MEMORY_MAX_MIB,ELASTICACHE_IAM_USER,ELASTICACHE_CACHE_NAME,ELASTICACHE_REGION,ELASTICACHE_SERVERLESS,AWS_REGION/AWS_DEFAULT_REGION(the IAM region whenELASTICACHE_REGIONis unset) andNEXT_DEPLOYMENT_ID(scopes the keys, see above).ELASTICACHE_AUTH_TOKENis ignored whenELASTICACHE_IAM_USERis set.
Deprecated, removed in 16.5
The removal ships with the package's first 16.5 release, which follows Next.js 16.5. That release is 16.5.2, because 16.5.0 and 16.5.1 were published by mistake in October 2026 and cannot be reused.
Each of these still works in 16.4.x and logs one deprecation warning per process on first use (types and IMPLICIT_TAG_PREFIX are marked @deprecated in JSDoc only). A deprecated factory does not add a second line for the class it builds.
createCacheHandler→withCache(nextConfig), orcreateRemoteCacheHandler/createLocalCacheHandlerin a custom handler file.createRedisDataCacheHandlerandcreateIoredisAdapter→withCache, orcreateRemoteCacheHandler({ client })with the ioredis client.createMemoryDataCacheHandler→withCache, orcreateLocalCacheHandler.createCacheConfig→withCache(nextConfig);createCacheConfigWithProfiles→withCache(nextConfig, { handlers }). Called the 16.4.2 way they return the same config; for the type, see "Types" above.MemoryCacheHandler,createMemoryCacheHandler→withCachewithoutREDIS_URL.CompositeHandler,createCompositeHandler→withCache; every handler has a built-in memory fallback.RedisCacheHandler'sredisoption →client(an ioredis client) orurl. ItstagPrefixoption is ignored → remove it.RedisCacheHandler'sdefaultTTLas anything but a number → a number of seconds. A numeric string ("60") is converted; a falsy value, a string that isn't a positive number, or any other non-number (0,NaN,null,"",false,true,"0","-5","1h", an object) is dropped, so entries get the 365-day default. Each logs a warning once per distinct value. A negative or infinite number throws when the handler is created (16.4.2 cached nothing with those).RedisCacheHandler#close()→closeCache(), which it calls.bufferToString,stringToBuffer,isImplicitTag,jsonReplacer,jsonReviver,calculateLifespan,isExpired,IMPLICIT_TAG_PREFIX→ no replacement (internal to the Next 14 handlers).- Types:
CacheValue→PageCacheValue,CacheHandlerValue→PageCacheHandlerValue,CacheHandlerContext→PageCacheHandlerContext,CacheHandlerGetMeta→PageCacheGetContext,RedisCacheHandlerOptions→PageCacheHandlerOptions(it is an alias).CacheHandler,CacheHandlerOptions,LifespanParameters,CompositeHandlerOptions,MemoryCacheHandlerOptions,CacheHandlerConfig,DataCacheHandlerOptions,CacheHandlerType,MemoryDataCacheHandlerOptions,RedisDataCacheHandlerOptions,RedisClientandIoredisLikego with the APIs above.DataCacheEntry,DataCacheHandlerandTimestamp(Next's own contract) are not deprecated.
RedisCacheHandler itself is not deprecated: it is the 16.4.3 page handler.
Added
withCache(nextConfig)configures the whole cache. It pointscacheHandlerat the page handler,cacheHandlers.remoteat Redis andcacheHandlers.defaultat per-instance memory with shared tag invalidation. Each directive keeps Next's meaning:"use cache"values stay in each instance's memory,"use cache: remote"entries go to Redis, and"use cache: private"never reaches a handler. WithREDIS_URLset, pages, route handlers, cachedfetchand"use cache: remote"are shared across instances, and every revalidation reaches every instance,"use cache"memory included. Without it everything stays in memory. Options:pages: false(keep your owncacheHandler),handlers(your own handler files) andmemory; an unknown option throws. It works withcacheComponents: trueand with Next's deprecatedexperimental.useCache, and accepts a function-formnext.config. Unlesspages: false, it also setscacheMaxMemorySize: 0, keeping a value you set (with a warning if it is not 0). Runtime settings come from the environment (REDIS_URL,CACHE_KEY_PREFIX,CACHE_REDIS=off,CACHE_DEBUG=1), so they work the same undernext start, standalone and Docker. Thememoryoption does not reach a standaloneserver.js, which does not loadnext.config; setCACHE_MEMORY_*there.- A Next 16 page handler.
RedisCacheHandleris rewritten to Next 16'scacheHandlercontract, keeping its name and constructor. It honoursx-next-cache-tags(implicit path tags included, Pages Router values too), soft tags,fetchtags andcacheControl, sorevalidateTagandrevalidatePathinvalidate pages and route handlers on every instance. Revalidation is timestamp-based: each read fetches the entry's tag records in the same round trip as the entry (a second one the first time a process reads that entry), so the next read on any instance sees a revalidation.revalidateTag(tag, "max")serves the page once more and regenerates it in the background.createPageCacheHandler(options)builds one for a custom handler file. - Distributed tag coordination for
"use cache", following Next's documented pattern. Values stay in each instance's memory.updateTagswrites the revalidation to Redis,refreshTagssyncs it into local state at most every 100 ms, with concurrent requests sharing one sync, andgetExpirationreads local state only. A failed or skipped tag check is a miss, never a stale hit. While Redis is reachable,revalidateTagresolves only after Redis has answered the write. When Redis is not ready, it returns after waiting at most 500 ms for the connection; the revalidation applies in this process and is sent when Redis is back, and a failed write is queued again. Instances compare times on wall clock, allowing 5 s of skew between machines. - Connection tuning and outage handling for every handler, beyond the shared connection and timeout above: a connection that times out
maxConsecutiveTimeouts(3) times in a row is dropped and redialled, and timed-out commands are not resent; startup waits at most 500 ms for the first connection. Aclientyou pass in is never disconnected or reconfigured: the package's own commands on it are bounded bycommandTimeout, and 3 timeouts in a row send the handlers to memory for 2 s at a time.connectTimeout,socketTimeoutandmaxConsecutiveTimeoutsare options of every factory and offlushCache. Handlers given different settings for the same server open separate connections, with a warning. - A binary entry format. Page and
"use cache: remote"entries store bytes raw instead of base64 JSON, the duplicated/_fullRSC payload once, and entries of 16 KB and more compressed with Brotli (quality 4, off the event loop). Entries over 64 MB are stored uncompressed, and a compressed entry claiming more than 64 MB is a miss. A corrupt or unknown entry is a miss.CACHE_COMPRESSION=offstores entries uncompressed. - A bounded memory budget. The in-process stores (
"use cache"values, page and"use cache: remote"entries without Redis, their outage copies, entry tag lists) share one budget per runtime, sized from V8's heap limit (the default is under Logs & ops; 98 MiB wherenode:v8is unavailable), split by fixed ratios. Keys count toward a store's size, and an entry larger than its store's cap is not kept in memory. Invalid values and unknownmemorykeys throw; a budget over half the heap limit logs a warning. See "Memory" in docs/operations.md, including pod sizing. - Tag records are bounded. Redis 7.4+ and Valkey 9+ expire them with hash-field TTLs, and standalone servers also prune them in the background. A record is kept until 365 days plus 10 s after its later deadline; flush records are kept. A Cluster without hash-field TTLs keeps every record until flushed and logs a rate-limited warning. Records held in process memory follow the same horizon.
flushCache({ scope })deletes the current build's keys ("build", the default) or every key under the prefix ("all") with SCAN + UNLINK, on every Cluster master and on ElastiCache Serverless (tested against a real Serverless Valkey 8.1 cache), and empties the calling process's memory stores. It records the flush as a revalidation, so older build output is not served again. On a Cluster, keys that still fail after a slot-migration redirect are reported as{ deleted, partial: true, failed }; a node that stops answering makes it throw. It refusesscope: "all"with an empty key prefix. Called before any handler has connected (a script, a test), it opens its own connection and closes it before returning. Put it behind your own authenticated route; the package ships no endpoint.closeCache()lets a test run, script or CLI exit: it closes every connection the package opened, empties its memory stores and drops its tag state, after sending unacknowledged revalidations one last time. It never fails because Redis is down and never closes aclientyou passed in. Handlers created before it reconnect on their next call, except the deprecatedcreateCacheHandler. For a test suite:afterAll(() => closeCache()).setCacheLogger(logger)(ininstrumentation.ts) sends every warning, error andCACHE_DEBUGline to your logger (errorandwarn, optionallyinfoanddebug); docs/operations.md has a one-line adapter for pino and bunyan. Custom handler files can passloggerinstead. A logger that throws never fails a cache call.createRemoteCacheHandlerandcreateLocalCacheHandlerfor custom handler files.createRemoteCacheHandlertakesdefaultTTL, the seconds to keep an entry whoseexpireis infinite (default 24 hours, at most 365 days); a value that is not a finite number greater than 0 throws.- ElastiCache IAM authentication. Set
ELASTICACHE_IAM_USER,ELASTICACHE_CACHE_NAMEandELASTICACHE_REGION(elseAWS_REGIONorAWS_DEFAULT_REGION) withELASTICACHE_ENDPOINTor arediss://REDIS_URL; serverless caches are detected from a.serverless.endpoint, or setELASTICACHE_SERVERLESS=true. In a custom handler file use theiamoption; for your ownRedisorClusterclient (needed for cluster-mode-enabled ElastiCache) callattachElastiCacheIamAuth(client, options). Tokens are signed with the AWS SDK's default credential chain and refreshed every 10 minutes, and sent asAUTHon the live connection, so the 12-hour connection limit never closes it. The token is never logged. It needs TLS, Valkey 7.2+ or Redis OSS 7.0+, and the three optional peer dependencies; misconfiguration throws at startup. 16.4.2 had no IAM support: a token passed as a password expired after 15 minutes, so reconnects failed. See "IAM authentication" in docs/redis.md. createCacheHandlergains the shared connection, the outage fallback and the optionsclient,memoryFallback,fallbackTTL,commandTimeout,defaultTTL,tagTTL(366 days),errorLogIntervalMsandsuppressIsrWarning. Agetwaits for an in-flightsetof the same key in the process, and an entry that has already expired is not stored.CACHE_DEBUG=1(ordebug: true) logs hits, misses and state changes, and each value's size ongetandset.
Changed
- Importing the package no longer loads
ioredis; 16.4.2 loaded it at import throughRedisCacheHandler. withCache's handlers never keep or serve a value past 365 days (cacheLife("max")'sexpire), in Redis or any memory store.createCacheHandlerkeeps its 366-day cap.createCacheHandler's tagexpireddeadline no longer deletes an entry written after it, as in Next, and an entry's tags are checked concurrently.jsonReplacer/jsonReviverstore a Buffer as base64 at every nesting level, not only at the top.- Connection options must be serializable: options that refer to themselves throw when the handler is created.
- Logged errors are scrubbed. ioredis attaches the failed command, arguments included, to a Redis error, so an
AUTHfailure could log the password. Every warning and error now logs errors with only their name, message, stack and network fields, objects without password-like keys, and URLs without their user information. Other free text in a message is not scrubbed.
Fixed
- Prerendered routes are served from the build output. A custom
cacheHandlerreplaces Next's file system cache, so with 16.4.2 prerendered pages and route handlers (sitemaps, robots, manifests) were rendered again on their first request. On a miss the page handler now reads the build output through Next's ownFileSystemCache, with the usual tag check, and writes it to Redis. A dynamic route's prerendered fallback shell is served too. A route with a revalidate period is served from the build output only within1.5 × revalidate(capped atexpire) of the build. The build time comes fromcache-handler-build.json, which the page handler writes into.next/serverduringnext build; ship it with.next. - Route handlers with sequential
"use cache"calls are prerendered. WithcacheComponents, Next aborts a prerender that reads the clock throughDate, and the handlers did, so such a route was silently built as dynamic. The handlers no longer readDateduring a prerender. - Edge middleware no longer crashes. Next bundles the page handler into the edge middleware, where importing the package called
createRequireand failed every request with a 500. The handler entries import no Node built-in at load time, and on the edge runtime the handlers are no-ops (every read a miss). - An older revalidation arriving late no longer undoes a newer one. Tag records are merged in Redis by a single-key Lua script in the order the revalidations were made, and the tag index uses
ZADD GT.
Docs
- The README is a short landing page around
withCache. Reference material is indocs/: how-it-works.md, operations.md (flushing, closing, logging, memory), custom-handlers.md, migrating.md (from 16.4.2, fromRedisCacheHandlersubclasses, and from@fortedigital/nextjs-cache-handler/@neshca/cache-handler), redis.md (key layout and sizing) and api-reference.md (exports and deprecations). cacheMaxMemorySizeis documented as having no effect withwithCache: it sizes Next's own caches, which Next does not use once the handlers are set.createCacheHandler's documentedkeyPrefixdefault is the real one,nextjs:data-cache:.
Changes between release candidates
For anyone who tested 16.4.3-rc.3 or earlier. What to change for 16.4.3-rc.4 and the final release:
withCache'suseCache: "memory" | "redis"option is removed. Plain"use cache"always stays in each instance's memory; write"use cache: remote"for entries that belong in Redis. PassinguseCachethrows, saying so.withCachethrows on any option other thanpages,handlersandmemory.MEMORY_ONLY_TAG("cache:memory-only") is removed and is an ordinary tag now: with Redis configured, every page and"use cache: remote"entry goes to Redis. Use"use cache"for per-instance data.- With Redis connected, the in-memory page and
"use cache: remote"stores no longer fill; only the outage fallbacks hold entries. createRedisCacheHandleris renamedcreatePageCacheHandler, with no alias. Its options type isPageCacheHandlerOptions;RedisCacheHandlerOptionsis a deprecated alias of it, removed in 16.5.RedisCacheHandleraccepts any non-numberdefaultTTLwith a warning instead of throwing; negative and infinite numbers still throw. The details are under "Deprecated" above.- The 16.4.2 APIs log a one-time deprecation warning and are removed in 16.5 (earlier candidates said "on the Next 17 line", and rc.4 said "16.5.0", a version npm can no longer publish).
Fixed
- Page-cache entries now retain their request's first lookup timestamp, preventing a render
completed after a tag invalidation from caching its old content as fresh. The production
e2e matrix holds a render across invalidation, including two processes sharing Redis.
Changed
- The handler is pinned to your Next.js minor. The
nextpeer dependency (it was^16.0.0) is now~<major>.<minor>.0of this release's own version (for example~16.4.0for 16.4.x,~16.5.0for 16.5.x). The package version follows Next.js, and the 16.5 releases remove the APIs deprecated in 16.4.3, so npm now refuses to install a handler whose minor does not match your Next.js (anERESOLVEerror) instead of pairing handler 16.5.x with Next 16.4; pnpm and Yarn warn. Each new Next minor moves the peer with it. Install with--save-exactand upgrade the handler and Next.js together.
Added
withCache(config, { useCache: "redis" })shares plain"use cache"entries across instances.
The default remains memory; explicithandlers.defaulttakes precedence.
Fixed
- Data-cache reads enforce the exact render-start hard-expiry deadline in memory, Redis and
outage fallback, including when rendering finishes between whole seconds.
What's Changed
Other Changes
- DEVRES-4325: share plain use cache entries through Redis by @mrjasonroy in #106
- chore: bump version to 16.4.3-rc.6 by @mrjasonroy in #108
Full Changelog: v16.4.3-rc.5...v16.4.3-rc.6
💜 Thank you to everyone who contributed to this release!