LazyLayersv0.5.3

Walkthrough

Follow one key through a cold load, a peer read, a database update, and a Redis outage.

Three instances use the quickstart setup with the same namespace and Redis service. Each has its own L1. Redis holds L2 and carries Pub/Sub events. The database holds the authoritative user record.

Request → Server A: L1 ─┐
Request → Server B: L1 ─┼→ Shared Redis L2 → Database loader on a miss
Request → Server C: L1 ─┘

       Redis Pub/Sub carries invalidations and eligible loaded values

1. A cold request loads user 42

Server A receives a request for user:42. Its repository calls:

Read through the cache
const user = await cache.getOrSet('user:42', async ({ signal } = {}) => {
  const row = await db.users.findById('42', { signal })
  return row ?? undefined
})

L1 and L2 both miss. A tracks the in-flight load and acquires the per-key Redis lease. It rechecks the cache before running the loader, so another instance's completed fill can avoid a second database query.

When the loader succeeds, Redis publication checks the lease token and writes the encoded value and TTL atomically. A lost lease cannot publish that result as a successful fill. L1 admission then retains the value when its memory budget allows it.

The execution paths explain the checks and failure branches in detail.

2. Concurrent callers share the work

More requests for user:42 arrive at A while its load is in flight. They join the same promise instead of starting another loader.

If B also misses, its Redis lease attempt contends with A. B waits for the shared value or retries when the lease becomes available. This wait is bounded. At the contention deadline, eligible stale data is returned or a DistributedLockTimeoutError reaches the caller.

Different keys use different leases. A bounded origin gate also limits concurrent loader work within each cache instance. See stampede protection for limits and timing.

3. The loaded value can warm peers

After a successful load, A publishes a set event carrying the value. B and C can admit it directly into their L1 without another database query. A filters out its own event.

Peer priming is conditional:

  • getOrSet loader results broadcast, including loads started through prewarm. See the API comparison for lower-level writes.
  • setupCache sets a 32 KiB broadcast ceiling. Oversized events are skipped, and peers can read L2 when needed. The low-level constructor has no ceiling unless you set one.
  • Delivery must succeed, and each peer's memory budget must admit the value.

L1 retains encoded bytes. A later L1 hit decodes a value rather than returning a permanently shared object reference.

4. A database write invalidates the key

Someone updates user 42. Commit the change before invalidating:

Invalidate after the commit
const updated = await db.users.update('42', { name: 'Ada Lovelace' })
await cache.invalidate('user:42')

A removes the cached value and related local state, fences older in-flight work, and publishes a del event. Peers that receive it drop their local copies. The next read reloads the committed row.

Invalidating before the database commit could let a concurrent loader read and cache the old row again. Invalidation is still separate from the database transaction, so applications that must retry a failed invalidation need durable delivery logic of their own.

5. A late event arrives

Suppose C has applied generation 8 and then receives a set event carrying generation 7 for the same key. C discards the older event. Event IDs suppress duplicates, and source filtering prevents self-echoes.

These checks do not establish a global order across every writer or make pattern invalidation atomic. Read event ordering before relying on stronger consistency.

6. Redis becomes unavailable

Redis failures are treated as cache failures rather than authoritative answers. L2 can be skipped, and reads can reach the origin loader. Circuit breakers and bounded queues limit work against failing dependencies.

With managed Redis Pub/Sub, a subscription gap also makes local invalidation state untrusted. The cache bypasses L1 and local fallback state until reconnect recovery restores trust. Do not assume that every L1 hit remains usable during a bus outage.

The request can still fail if the origin fails, the work queue is full, or a deadline is reached without an eligible stale value. Fail-open caching does not guarantee a successful application response.

Where to next

On this page