Cloudflare Workers
Compose the current Cache source with Cloudflare KV, a Coordinator Durable Object, and the Workers factory.
The current source exposes a Cloudflare entry point at @astilba/cache/cloudflare. Its createWorkersCache() factory combines the portable kernel with a bounded memory L1, Cloudflare KV L2, a Coordinator Durable Object, a live WebSocket Bus, and reconnecting recovery behavior.
See what the factory owns
Section titled “See what the factory owns”You provide one stable registry name and two Cloudflare bindings:
import { env } from "cloudflare:workers"import { createWorkersCache } from "@astilba/cache/cloudflare"
export const cache = createWorkersCache({ name: "storefront", kv: env.CACHE_KV, coordinator: env.COORDINATOR,})The Worker entry must separately export the Durable Object class so Wrangler can bind it:
export { Coordinator } from "@astilba/cache/cloudflare"
export default { async fetch(): Promise<Response> { return new Response("Worker ready") },} satisfies ExportedHandlerThe name is one identity, not a display label. The factory uses it as:
- the named Coordinator Durable Object address;
- the Registry and replication-mirror identifier;
- the Cache namespace used in canonical keys.
Keep it stable for the lifetime of the cache domain. Changing it addresses a different Durable Object and a different keyspace.
The factory also supplies:
- a Workers wall-clock
Clockand randomRngat the platform boundary; memory({ clock, maxEntries: 512, maxBytes: 5_000_000 })as L1;cloudflareKV()as L2;doRegistry()against the named Coordinator;- a self-redialing
doBus()connection; - eventual consistency, live Registry checks for unknown knowledge, the default HTTP retry classifier, and a 30-second reader heartbeat interval unless you override those fields in
defaults.
Configure the Worker bindings
Section titled “Configure the Worker bindings”The Worker must export Coordinator, bind that class as a SQLite-backed Durable Object, and make one KV namespace visible under two binding names:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "storefront-worker", "main": "src/worker.ts", "compatibility_date": "2026-07-15",
"durable_objects": { "bindings": [ { "name": "COORDINATOR", "class_name": "Coordinator" } ] },
"migrations": [ { "tag": "v1", "new_sqlite_classes": ["Coordinator"] } ],
"kv_namespaces": [ { "binding": "CACHE_KV", "id": "<your-kv-namespace-id>" }, { "binding": "REGISTRY_KV", "id": "<your-kv-namespace-id>" } ],
"vars": { "REGISTRY_HEARTBEAT_MS": "30000" }}Both KV bindings point to the same namespace. CACHE_KV is the L2 Store the reader sees; REGISTRY_KV is where the Coordinator writes replication pointers, deltas, and snapshots. If they point at different namespaces, the reader cannot recover from the mirror the Coordinator produced.
New Durable Object classes use new_sqlite_classes. See Cloudflare’s Durable Object migration guide before merging this into an existing migration history.
Decide whether to enable idle heartbeats
Section titled “Decide whether to enable idle heartbeats”createWorkersCache() configures the reader to expect a 30-second heartbeat interval. The Coordinator cannot set its own deployment variables, so you must separately opt its idle heartbeat in with:
"vars": { "REGISTRY_HEARTBEAT_MS": "30000" }Leaving the variable unset keeps idle heartbeats dormant. That avoids a recurring Durable Object alarm and KV write for every idle registry, but a reader can become conservatively suspicious and pay for live checks until it reconverges. Setting it to 30000 matches the factory’s reader default.
If you override defaults.heartbeatInterval, update the deployment variable deliberately as well. The two settings are not synchronized by the library.
Use the cache in a Worker
Section titled “Use the cache in a Worker”Once constructed, application code uses the same portable API:
import { t } from "@astilba/cache"import { cache } from "./cache.server"
export async function getProduct(productId: string) { return cache.getOrSet({ key: `product:${productId}`, tags: [t`product:${productId}`], factory: ({ signal }) => loadProduct(productId, signal), })}After updating the source of truth, invalidate the same dependency:
await saveProduct(productId, input)await cache.delete({ tag: t`product:${productId}` })The mutation reaches the authoritative Coordinator. Active isolates receive live Bus events; suspect readers can recover through the KV mirror. A strong read performs a live check before serving a stored entry and before filling a strong miss.
Know the operational boundary
Section titled “Know the operational boundary”The source path currently includes:
- KV value-size rejection and write-failure classification;
- Coordinator command journaling, coalesced flushes, snapshots, and Registry RPC;
- WebSocket Bus delivery with scope checks and jittered redial backoff;
- reactive read-path recovery plus an out-of-band polling state machine;
- a request-piggyback poll driver when the React Router middleware is used.
It does not yet provide:
- an npm release or supported upgrade policy;
- elapsed TTL, grace, or age enforcement;
- journal checkpointing and truncation for a long-lived Coordinator;
- a production Lock or CDN purge driver;
- automatic shared-response
Cache-Tagemission; - the chaos demo and deployed consistency measurements that complete the Workers release path.
Continue with React Router if that is your server framework, Driver implementations for component-level status, or Implementation status for kernel limitations.