Node.js cache quickstart
Install a TypeScript cache npm package, start with in-memory L1, then add Redis L2, read-through loading, and invalidation.
Install the lazy-layers-cache npm package to start with a runnable in-memory L1 cache, then connect the same API to Redis L2 when multiple Node.js instances need to share data. You need Node.js 20 or 22+. The first example needs no database or Redis.
Install the package
npm install lazy-layers-cacheRun your first cached read
Save this file and run node cache-demo.mjs.
import { setupCache } from 'lazy-layers-cache'
const cache = await setupCache({ namespace: 'demo', redis: false })
let loaderCalls = 0
try {
const loadUser = async () => {
loaderCalls++
return { id: '42', name: 'Ada' }
}
await cache.getOrSet('user:42', loadUser)
const user = await cache.getOrSet('user:42', loadUser)
console.log(user.name, loaderCalls) // Ada 1
await cache.invalidate('user:42')
await cache.getOrSet('user:42', loadUser)
console.log(loaderCalls) // 2
} finally {
await cache.close()
}The first read runs the loader. The second reuses the value. Invalidating the key makes the next read load it again.
Share values across instances
Set REDIS_URL to your Redis 6+ service. Create one cache per application process:
import { setupCache } from 'lazy-layers-cache'
export const cache = await setupCache({
namespace: 'users-api',
redis: { required: true },
})For the TypeScript example, define the value your application reads:
export interface User {
id: string
name: string
}Use the same namespace and Redis service for instances that share values. Setup waits for Redis and the event subscription to be ready. required: true prevents a missing URL from silently starting isolated L1 caches.
Connect your database reads and writes
The following repository assumes you provide src/db.js. Adapt the database calls to your client. Return undefined for a missing record, and pass the abort signal when your client supports cancellation.
import { cache } from '../cache/index.js'
import { db } from '../db.js'
export function getUser(id) {
return cache.getOrSet(`user:${id}`, async ({ signal } = {}) => {
const user = await db.users.findById(id, { signal })
return user ?? undefined
})
}
export async function updateUser(id, patch) {
const user = await db.users.update(id, patch)
await cache.invalidate(`user:${id}`)
return user
}Commit the database write before invalidating. Invalidation clears cached state and publishes a delete event to peers. Event delivery is asynchronous, so it does not make the database write and every cached read atomic.
Close the cache on shutdown
After your server stops accepting requests and drains active work, call:
await cache.close()close() is idempotent. It disconnects the event bus, stops observability, and closes a Redis client created by setupCache. Your application closes any client it supplied itself.
What is already configured
In-flight dedupe shares concurrent same-key loads in one process. Redis per-key leases coordinate loads between instances. Loader timeouts, bounded origin work, stale fallback, negative caching, and circuit breakers are already active.
A successful loader can also prime peer L1 caches. setupCache limits the encoded broadcast to 32 KiB. Larger events skip priming, and memory pressure can prevent L1 admission. The configuration reference documents every default and override.
Redis or bus failures can degrade to origin reads. Your application must still handle loader errors, overload, and lock deadlines. See failure handling.
Add only what your application needs
| Need | Next step |
|---|---|
| Warm a known key before traffic | prewarm, using the same loader |
| Invalidate a key family | invalidateByPattern |
| Tune memory, TTLs, and concurrency | Production setup |
| Coordinate a durable payment or order attempt | Transaction coordination, a separate API |
Where to next
- Walkthrough follows one key through reads, writes, and failures.
- Production setup covers deployment and capacity choices.
- API reference gives the complete method list.