Skip to content
Sponsor

Inspect cache behavior

Witness stored entries with explain() and route operational events through telemetry sinks.

Astilba Cache exposes two complementary observability surfaces. cache.explain() takes a point-in-time witness of one public key, while telemetry reports operational events as they occur. Neither surface turns uncertainty into a stronger correctness claim.

Call explain(key) with the same application-facing key used by getOrSet():

const explanation = await cache.explain(`product:${productId}`)
switch (explanation.kind) {
case "present":
console.log({
tier: explanation.tier,
identity: explanation.identity,
verdict: explanation.verdict,
reader: explanation.reader,
})
break
case "absent":
console.log("not present", explanation.reader)
break
case "read-failed":
console.warn("presence unknown: a Store did not answer", explanation.reader)
}

The kind discriminant separates three materially different observations:

  • present means a decodable entry was found in L1 or L2;
  • absent means every probed tier answered and no decodable entry was found;
  • read-failed means at least one classified Store read failure was suppressed and no other tier supplied the entry, so presence is unknown.

A missing, undecodable, or codec-incompatible entry is a reportable { kind: “absent”, tier: “miss” } result only when every probed Store answered. A classified Store failure is not laundered into the same answer: if one tier fails while another contains only an undecodable or incompatible entry, the result remains read-failed. That arm deliberately has no tier or identity.

Result field What it witnesses
kind Whether the probe found an entry, proved absence, or could not establish either answer because a Store read failed.
key The application-facing key passed to explain().
tier On present, the first decodable copy observed in L1 or L2. On absent, miss. Absent from read-failed.
identity.tags The stored user and reserved tags.
identity.scope The stored scope literal, or an explicit unreadable marker.
identity.bornEpoch The invalidation epoch at which the entry was born.
identity.ttl Stored TTL evidence. Current entries report { kind: “not-stored” }.
identity.kind A value or negative entry.
verdict Current fresh, stale, dead, or unknown classification and the soft and hard epochs behind it.
reader The local reader’s applied epoch, suspicion state, terminal recovery state, and live-channel state.

Without a coordinated invalidation reader, a present entry receives the codec-only fresh verdict and zero epochs. That says no local invalidation authority exists to classify it otherwise; it is not evidence from a Registry check.

reader.channel distinguishes never-established, established, and lost. This matters because a Bus that never completed its first handshake can still have suspect: false: no continuity gap has occurred, but there is also no established live channel. Read both fields when diagnosing delivery health.

explain() reports what this Cache instance already knows. It deliberately does not:

  • perform a live Registry check;
  • trigger mirror recovery or resynchronization;
  • hydrate an L2 result into L1;
  • preserve a historical dependency graph;
  • accept a scope argument.

The method canonicalizes the key at the default public scope. It cannot directly address tenant or principal-derived variants. Within a React Router request, requestDependencies can still show the dependencies the current render has recorded, including their scope evidence and any l3 flag.

The L1 and L2 probes call each Store’s ordinary get(). Cache performs no write while explaining, but a Store may have read behavior of its own; for example, memory() updates LRU recency on a successful read.

Pass a sink directly for local use, or use TelemetryConfig when you need hosted pseudonymization or sink-failure reporting:

const cache = createCache({
namespace: "storefront",
clock,
rng,
l2,
telemetry: {
sink: (event) => logger.info(event),
onSinkError: (error) => logger.warn({ error }, "cache telemetry failed"),
},
})

Built-in event delivery swallows both synchronous sink throws and asynchronous sink rejections so observability code cannot fail a read, fill, or response. onSinkError is called for a swallowed failure; failures from that hook are swallowed too.

For a hosted sink, set hosted: true and provide a project salt. String fields other than the event type are HMAC-pseudonymized before delivery. Hosted mode without a salt suppresses the event instead of forwarding raw strings.

TELEMETRY_EVENTS is the public catalog, and TelemetryEventName is the union of its values.

Event Current source behavior
writeback_throttled A retryable throttled L2 write was suppressed and the origin result remained non-durable.
l2_write_error Another retryable L2 write failure was suppressed.
singleflight_option_mismatch Production mode ran incompatible same-key work separately.
store_read_suppressed A classified throttled or unavailable Store read failed. The serving path treated that tier as a miss; the event includes the structural code and canonical key.
neg_suppressed An opted-in 404 was suppressed in favor of a grace-eligible stale value, or the L2 negative-write guard refused it because L2 held a decodable value whose invalidation verdict was fresh or stale, or could not be established.
private_evicted A configured memory() sink observed a principal-scoped entry evicted by an LRU bound.
poll_tick_failed Request-driven recovery work rejected in the Workers carrier or React Router lifecycle integration.
l3_ineligible React Router demoted a managed response for budget, late_tag, scope, or scope_unreadable.
regid_divergence The Coordinator writes a Workers log when a derived Registry identity differs from its journaled identity.
bus_dial_failed The invalidation reader received lost without a currently established channel: an initial dial or a later redial did not complete its hello handshake.
strong_degraded A failed strong Registry check degraded that call to eventual because onUnavailable: “eventual” was configured. The current reason is registry_unreachable.
registry_degraded Reserved in the catalog; no current emit site.
state_stale Reserved in the catalog; no current emit site.

The catalog closes the event-name vocabulary, not every event payload. Treat fields other than type according to the specific event you consume.

The React Router middleware accepts its own telemetry and onSinkError options for poll_tick_failed and l3_ineligible. Pass the same sink explicitly if you want those events beside kernel events:

cacheMiddleware({ cache, telemetry: sink, onSinkError, waitUntil })

The memory() driver also accepts telemetry and onSinkError. It emits private_evicted only when memory pressure removes a usr:-scoped entry, with count and byte information but no key, hash, or tag. TTL expiry, explicit deletion, and replacement do not emit that event.

createWorkersCache() accepts telemetry for kernel and Workers-carrier events. It still fixes its internal L1 construction without a separate memory sink, so receiving private_evicted from a memory L1 requires direct composition with createCache() and memory({ telemetry }). React Router middleware telemetry is likewise an explicit option; pass the same sink to both surfaces when you want one event stream.

Continue with Cache HTTP responses for the response safety gate, Control cache sharing for private storage rules, or API reference for the complete types.