Migration Guide
This guide helps you migrate from popular Node.js caching libraries to Layercache. Each section includes before/after code examples, API mappings, and key differences.
Upgrading to 3.0
Layercache 3.0 is a major release because it changes operational defaults for Redis-backed deployments and HTTP middleware cache keys.
RedisTagIndex known-key shards
RedisTagIndex now defaults to 16 known-key shards. Existing deployments that used the previous single-set layout at <prefix>:keys are still read for compatibility, but you should migrate them before relying on the sharded layout in production:
npx layercache migrate-tag-index \
--redis rediss://redis.example.com:6379 \
--tag-index-prefix myapp:tag-index \
--known-key-shards 16
Use knownKeysShards: 1 only when you intentionally need the legacy layout during a staged rollout.
Production Redis URLs in the CLI
CLI commands now reject plaintext redis:// URLs when NODE_ENV=production, unless --allow-plaintext is passed:
NODE_ENV=production npx layercache stats --redis rediss://redis.example.com:6379
NODE_ENV=production npx layercache stats --redis redis://localhost:6379 --allow-plaintext
Implicit HTTP cache keys
Express and Hono middleware now remove common sensitive query parameters from implicit URL cache keys. This avoids storing secrets in cache keys, but entries written with older URL keys that included parameters such as api_key, private_key, or credentials will miss until refreshed.
Generation persistence
Persist generation rotations when several instances share the same cache:
import { CacheStack, RedisGenerationStore } from 'layercache'
const generations = new RedisGenerationStore({ client: redis })
const generation = await generations.getOrInitialize(1)
const cache = new CacheStack(layers, { generation })
const nextGeneration = await generations.bump()
cache.bumpGeneration(nextGeneration)
From node-cache-manager
Basic Setup
Before (node-cache-manager):
import { caching, multiCaching } from 'cache-manager'
import { redisStore } from 'cache-manager-redis-yet'
const memoryCache = await caching('memory', { max: 100, ttl: 60 * 1000 })
const redisCache = await caching(redisStore, {
url: 'redis://localhost:6379',
ttl: 300 * 1000
})
const cache = multiCaching([memoryCache, redisCache])
After (layercache):
import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
import Redis from 'ioredis'
const cache = new CacheStack([
new MemoryLayer({ ttl: 60_000, maxSize: 100 }),
new RedisLayer({ client: new Redis(), ttl: 300_000 })
])
Read-Through Fetch
Before:
const user = await cache.wrap('user:123', () => db.findUser(123))
After:
const user = await cache.get('user:123', () => db.findUser(123))
Set with TTL
Before:
await cache.set('user:123', user, 60000) // TTL in milliseconds
After:
await cache.set('user:123', user, { ttl: 60_000 }) // TTL in milliseconds
Delete
Before:
await cache.del('user:123')
After:
await cache.delete('user:123')
Clear All
Before:
After:
API Mapping
Key Differences
TTL is in Milliseconds
node-cache-manager uses milliseconds:
await cache.set('key', value, 60000) // 60 seconds
Layercache uses milliseconds:
await cache.set('key', value, { ttl: 60_000 }) // 60 seconds
Auto Backfill
node-cache-manager requires manual warming:
// After a cache miss in L1, L1 stays cold
const value = await cache.wrap('key', fetcher)
// Next request might still hit L2 instead of L1
Layercache auto-backfills L1:
// After a cache miss in L1, L1 is automatically backfilled
const value = await cache.get('key', fetcher)
// Next request hits L1 immediately
Stampede Prevention
node-cache-manager requires plugins:
// No built-in stampede prevention
// Need external solutions
Layercache has built-in stampede prevention:
// Enabled by default
// Multiple concurrent requests for same key share single fetcher
Tag Invalidation
node-cache-manager:
// Manual key tracking required
const userKeys = [`user:${id}`, `user:${id}:posts`, `user:${id}:profile`]
await Promise.all(userKeys.map(key => cache.del(key)))
Layercache:
await cache.set('user:123', user, { tags: ['user:123'] })
await cache.set('user:123:posts', posts, { tags: ['user:123'] })
await cache.invalidateByTag('user:123') // Deletes both
From keyv
Basic Setup
Before (keyv):
import Keyv from 'keyv'
import KeyvRedis from '@keyv/redis'
const keyv = new Keyv({
store: new KeyvRedis('redis://localhost:6379')
})
await keyv.set('user:123', user, 60000)
const user = await keyv.get('user:123')
After (layercache):
import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
const cache = new CacheStack([
new MemoryLayer({ ttl: 60_000 }),
new RedisLayer({ client: new Redis(), ttl: 300_000 })
])
await cache.set('user:123', user, { ttl: 60_000 })
const user = await cache.get('user:123')
Read-Through Fetch
Before:
// No built-in read-through
let user = await keyv.get('user:123')
if (!user) {
user = await fetchUser(123)
await keyv.set('user:123', user, 60000)
}
After:
// Built-in read-through fetch
const user = await cache.get('user:123', () => fetchUser(123))
API Mapping
Key Differences
Multi-Layer is Native
keyv requires plugins:
// Multi-layer is not native
// Requires custom adapters
Layercache has native multi-layer:
const cache = new CacheStack([
new MemoryLayer({ ttl: 60_000 }),
new RedisLayer({ client: redis, ttl: 300_000 })
])
// Reads cascade through layers with auto backfill
Read-Through Fetch
keyv requires manual checks:
let value = await keyv.get('key')
if (value === undefined) {
value = await fetcher()
await keyv.set('key', value)
}
Layercache has built-in read-through:
const value = await cache.get('key', fetcher)
Namespaces
keyv:
const userCache = new Keyv({ namespace: 'users' })
const postCache = new Keyv({ namespace: 'posts' })
Layercache:
const userCache = cache.namespace('users')
const postCache = cache.namespace('posts')
// Full CacheStack API on namespaces
await userCache.set('123', data) // Stored as "users:123"
await userCache.clear() // Only deletes "users:*"
From cacheable
Basic Setup
Before (cacheable):
import { Cacheable } from 'cacheable'
const cache = new Cacheable({ ttl: '1h' })
await cache.set('key', value)
After (layercache):
import { CacheStack, MemoryLayer } from 'layercache'
const cache = new CacheStack([
new MemoryLayer({ ttl: 3_600_000 })
])
await cache.set('key', value)
API Mapping
Key Differences
cacheable uses string durations:
const cache = new Cacheable({ ttl: '1h' })
Layercache uses numeric milliseconds:
const cache = new CacheStack([new MemoryLayer({ ttl: 3_600_000 })])
Multi-Layer Orchestration
cacheable:
// Limited multi-layer support
// Manual coordination required
Layercache:
const cache = new CacheStack([
new MemoryLayer({ ttl: 60_000 }),
new RedisLayer({ client: redis, ttl: 300_000 }),
new DiskLayer({ ttl: 3_600_000 })
])
// Automatic cascading reads and writes
Distributed Consistency
cacheable:
// No built-in distributed features
// Manual implementation required
Layercache:
import { RedisInvalidationBus, RedisTagIndex } from 'layercache'
const bus = new RedisInvalidationBus({ publisher: redis })
const tagIndex = new RedisTagIndex({ client: redis })
const cache = new CacheStack([/* ... */], {
invalidationBus: bus,
broadcastL1Invalidation: true,
tagIndex
})
Stampede Prevention
cacheable:
// No built-in stampede prevention
Layercache:
const cache = new CacheStack([/* ... */], {
stampedePrevention: true // Enabled by default
})
Operational Migration Tips
Replace Ad-Hoc Redis Key Scans
Before:
# Manual Redis scans
redis-cli --scan --pattern "user:*" | xargs redis-cli del
After:
# Use Layercache CLI
npx layercache keys --redis redis://localhost:6379 --pattern "user:*"
npx layercache invalidate --redis redis://localhost:6379 --pattern "user:*"
Replace Manual Prefill Scripts
Before:
// Custom warm-up script
for (const key of criticalKeys) {
const val = await fetch(key)
await redis.set(key, JSON.stringify(val), 'EX', 300)
}
After:
// Built-in warm() method
await cache.warm(
criticalKeys.map(key => ({
key,
fetcher: () => fetchByKey(key),
priority: 10
})),
{ concurrency: 4, continueOnError: true }
)
Replace Custom Stats Endpoints
Before:
// Custom stats endpoint
app.get('/stats', (req, res) => {
res.json({
hits: myCounter.hits,
misses: myCounter.misses
})
})
After:
import { createCacheStatsHandler } from 'layercache'
app.get('/cache/stats', createCacheStatsHandler(cache))
Replace Manual Tag Tracking
Before:
// Manual tag-to-key mapping
const tagMap = new Map()
async function setWithTags(key, value, tags) {
await redis.set(key, JSON.stringify(value))
for (const tag of tags) {
const keys = tagMap.get(tag) || []
keys.push(key)
tagMap.set(tag, keys)
}
}
async function invalidateByTag(tag) {
const keys = tagMap.get(tag) || []
await Promise.all(keys.map(key => redis.del(key)))
tagMap.delete(tag)
}
After:
// Built-in tag support
await cache.set('user:123', user, { tags: ['user:123'] })
await cache.invalidateByTag('user:123')
Replace Custom Invalidation Logic
Before:
// Manual prefix-based invalidation
async function invalidatePrefix(prefix) {
const keys = await redis.keys(`${prefix}*`)
await Promise.all(keys.map(key => redis.del(key)))
}
After:
// Built-in prefix invalidation
await cache.invalidateByPrefix('user:123:')
Replace Custom Locking for Stampede Prevention
Before:
// Manual distributed locking
async function getWithLock(key, fetcher) {
const lockKey = `lock:${key}`
const lock = await redis.set(lockKey, '1', 'PX', 5000, 'NX')
if (lock === 'OK') {
try {
const value = await fetcher()
await redis.set(key, JSON.stringify(value))
return value
} finally {
await redis.del(lockKey)
}
} else {
// Wait and retry
await sleep(100)
return getWithLock(key, fetcher)
}
}
After:
// Built-in distributed single-flight
import { RedisSingleFlightCoordinator } from 'layercache'
const coordinator = new RedisSingleFlightCoordinator({ client: redis })
const cache = new CacheStack([/* ... */], {
singleFlightCoordinator: coordinator,
singleFlightLeaseMs: 30000
})
const value = await cache.get(key, fetcher)
Migration Checklist
Phase 1: Setup
Phase 2: Code Changes
Phase 3: Testing
Phase 4: Optimization
Common Migration Issues
TTL Confusion
Issue: Keeping old second-based TTL values after migrating.
Solution:
// Wrong
await cache.set('key', value, { ttl: 60 }) // 60 milliseconds
// Correct
await cache.set('key', value, { ttl: 60_000 }) // 60 seconds
Issue: Unable to invalidate related keys.
Solution:
// Add tags when setting
await cache.set('user:123', user, { tags: ['user:123'] })
await cache.set('user:123:posts', posts, { tags: ['user:123'] })
// Invalidate by tag
await cache.invalidateByTag('user:123')
Namespace Confusion
Issue: Not using namespaces for scoped caches.
Solution:
// Use namespaces instead of separate caches
const userCache = cache.namespace('users')
const postCache = cache.namespace('posts')
await userCache.set('123', data) // Stored as "users:123"
Not Using Read-Through
Issue: Still using manual miss handling.
Solution:
// Old way
let value = await cache.get('key')
if (!value) {
value = await fetcher()
await cache.set('key', value)
}
// New way
const value = await cache.get('key', fetcher)
Need Help?
If you run into issues during migration:
- Check the API Reference for detailed method documentation
- Review examples for common patterns
- Open an issue with your current setup
We're happy to help you find the right approach for your use case!