cache-manager
cache-manager is an asynchronous Node.js cache facade built on Keyv stores. It gives applications one API for get, set, bulk operations, deletion, clearing, expiration lookup, function-result caching, refresh-ahead, events, and shutdown. One cache can layer memory before Redis, Postgres, MongoDB, SQLite, or other Keyv adapters, backfilling faster stores after a lower tier hits. Version 7 returns undefined on misses and includes an unbounded default in-memory store unless you configure a different Keyv backend.
cache-manager 7 is a capable adapter-friendly choice for Node services that need wrap and layered caches. Configure bounds and persistence deliberately, treat nonBlocking as a best-effort mode, and budget real migration work if your code predates the Keyv-based version 6 API.
Use it if
- A Node service needs one cache API that can move from process memory to Keyv-backed Redis or another remote store
- You need layered L1 and L2 caching with automatic reads from higher-priority stores and backfill after a lower-tier hit
- Expensive async work needs wrap-based request coalescing and optional refresh-ahead within one process
- Your framework or existing code already expects cache-manager semantics and version 7's undefined-on-miss behavior
- You need distributed stampede protection: wrap coalesces concurrent work by cache ID and key inside one JavaScript process, not across replicas
- You plan to enable nonBlocking on layered reads without accepting false misses: the source uses Promise.race, so a fast store returning undefined can win before a slower store returns a value
- You want persistence with no adapter setup: the default store is process-local memory, disappears on restart, and has no size limit or eviction policy configured by cache-manager
- You are upgrading from version 5 or earlier and expect old stores to drop in: version 6 moved adapters to Keyv, and version 7 changed cache misses from null to undefined
- You need strict write durability while nonBlocking is enabled: set, delete, and clear return before all store promises settle, and the implementation does not await the aggregate promise in that mode
Setup reality
npm install cache-manager gives you an ESM and CommonJS build, bundled TypeScript declarations, Keyv, and @cacheable/utils. createCache() works immediately with an in-memory Keyv instance whose serialization is disabled, but it is unbounded and per-process. Production persistence means choosing and installing a Keyv adapter separately, supplying its URL and credentials, and deciding whether values can be serialized safely. External Keyv instances use their own serializer settings; the README warns that Symbol values and types such as Uint8Array can come back incorrectly under JSON serialization unless you disable or replace serialization. TTL values are milliseconds. cache.ttl(key) returns the absolute expiration timestamp from Keyv, not milliseconds remaining, despite wording in parts of the README. Misses are undefined in v7, though several method examples lower in the README still show null. Multi-store order is priority order. Blocking mode checks stores sequentially, fills missing values from lower tiers for mget, and backfills earlier stores after wrap hits. nonBlocking changes the correctness contract by racing reads and returning before mutations settle, so use it only when best-effort caching is acceptable and watch adapter error events. refreshThreshold returns stale data and starts a background refresh; a slow worker can race key expiry, and no refresh occurs without a TTL. Remote adapters need explicit disconnect() during shutdown. Cache keys, invalidation, tenant separation, value versioning, and authorization boundaries remain application responsibilities.
Patterns
Create the default in-memory cachecreate-memory-cache
import { createCache } from 'cache-manager';
const cache = createCache({
ttl: 60_000,
});The default store is local to this process and has no configured size limit. Use a bounded memory adapter for untrusted or high-cardinality keys.
Set and read a typed valueset-and-get
type User = { id: string; name: string };
await cache.set<User>('user:42', { id: '42', name: 'Ada' }, 30_000);
const user = await cache.get<User>('user:42');
if (user === undefined) {
console.log('cache miss');
}TTL is milliseconds. Version 7 returns undefined, not null, for a missing or expired key.
Write and read several keysset-many-values
await cache.mset([
{ key: 'feature:a', value: true },
{ key: 'feature:b', value: false, ttl: 5_000 },
]);
const [a, b, missing] = await cache.mget<boolean>([
'feature:a',
'feature:b',
'feature:none',
]);mget preserves key order and uses undefined for misses. In blocking multi-store mode it fills missing positions from lower tiers.
Delete one key or flush the cachedelete-cache-entries
await cache.del('user:42');
await cache.mdel(['feature:a', 'feature:b']);
// Use sparingly: clears every configured store.
await cache.clear();clear affects the entire configured cache namespace. Prefer versioned or tenant-scoped keys when broad invalidation is risky.
Cache and coalesce an expensive callwrap-expensive-function
const user = await cache.wrap(
'user:42',
() => database.users.findById('42'),
60_000,
);Concurrent wraps for the same cache ID and key coalesce inside this process only. Multiple service replicas can still run the worker simultaneously.
Choose TTL from the computed valueset-dynamic-ttl
const session = await cache.wrap('session:abc', loadSession, {
ttl: (value) => value.isPremium ? 60_000 : 10_000,
});Dynamic TTL is supported by wrap options. Return milliseconds and ensure every branch returns a valid positive duration for expiring entries.
Refresh a hot value in the backgroundrefresh-before-expiry
const cache = createCache({
ttl: 60_000,
refreshThreshold: 10_000,
});
const catalog = await cache.wrap('catalog:v3', loadCatalog);A hit with less than the threshold remaining returns stale data immediately and refreshes in the background. Without a TTL, refresh does not trigger.
Return a value with its expiration timestampinspect-raw-expiry
const result = await cache.wrap('rates:usd', loadRates, {
ttl: 30_000,
raw: true,
});
console.log(result.value, new Date(result.expires));expires is an absolute millisecond timestamp. Supply a TTL when requesting raw output so the expiration is meaningful.
Layer bounded memory in front of Redisconfigure-tiered-cache
import { CacheableMemory } from 'cacheable';
import { Keyv } from 'keyv';
import KeyvRedis from '@keyv/redis';
const memory = new Keyv({
store: new CacheableMemory({ ttl: 60_000, lruSize: 5_000 }),
});
const redis = new Keyv({ store: new KeyvRedis(process.env.REDIS_URL) });
const cache = createCache({ stores: [memory, redis] });Store order is priority order. Redis adds credentials, network failure modes, serialization choices, and a shutdown requirement.
Disable Keyv JSON serialization for binary valuescache-binary-values
import { Keyv } from 'keyv';
const memory = new Keyv();
memory.serialize = undefined;
memory.deserialize = undefined;
const binaryCache = createCache({ stores: [memory] });
await binaryCache.set('bytes', new Uint8Array([1, 2, 3]));The default cache-manager memory store already disables serialization. Configure external Keyv instances explicitly when JSON would change the value.
Log cache errors and refreshesobserve-cache-events
cache.on('set', ({ key, error }) => {
if (error) console.error('cache set failed', key, error);
});
cache.on('refresh', ({ key, error }) => {
if (error) console.error('refresh failed', key, error);
});Background refresh failures arrive through refresh events. Add listeners before traffic if cache failures must be observable.
Close remote adapters during shutdowndisconnect-cache-stores
async function shutdown() {
await cache.disconnect();
process.exitCode = 0;
}
process.once('SIGTERM', () => {
void shutdown();
});Remote adapters such as Redis may keep sockets open. Await disconnect only when the application is actually shutting down.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cacheable | npm | You want the same project's newer L1 and L2 caching framework with richer memory-cache controls |
| keyv | npm | You only need a small storage-agnostic key-value cache and prefer to build layering or wrapping yourself |
| lru-cache | npm | A bounded in-process cache with explicit eviction behavior is enough and remote stores are unnecessary |
| @keyv/redis | npm | You want direct Keyv access to Redis without cache-manager's tiers, wrap, and event facade |