Cloudflare
Cloudflare already guarantees a single instance of a Durable Object globally and serializes requests to it. That is the virtual-actor contract — so the platform is the cluster.
Installation
pnpm add @sigx/actors-cloudflareWhy it is small
This package needs none of the machinery @sigx/actors/cluster
uses to rebuild that contract: no membership heartbeats, no activation directory, no
HMAC-authenticated host-to-host mount.
There is no HMAC because a Durable Object stub is not network-reachable — holding the binding is the capability grant, and guards run once at the public edge. And there is no 421 retry because ref → object id is a pure function, so a mismatch is a config bug rather than a race.
Storage
const storage = durableObjectStorage(state.storage);
DO storage is strongly consistent and single-threaded per object, so the runtime's etag compare-and-set holds without a transaction.
It implements appendText, so
ctx.append writes one storage key per
entry instead of a full save; a full ctx.save() compacts the log, and load replays it
through applyEntry. The real DurableObjectStorage needs no configuration for this. A custom
DurableStorage gets the append path only if it provides the optional list({ prefix });
without it every append is a full save.
Placement
A ref maps to its Durable Object by name — the platform is the directory — so a DO-hosted actor
has no placement strategy to apply. placement on such an actor is refused:
- At compile time, through the app-bound
defineActor, when the app installsdurableObjects(...)or, in anappfactory,durableObjectsHosted(). Both carryneveras their placement type. - At dispatch otherwise. A strategy tagged
backend: 'cluster'— every@sigx/actors/clusterpolicy — throws, naming the type and the strategy. Any other declared strategy logs one__DEV__warning per type.
Reminders
const reminders = durableObjectReminders({
storage: state.storage,
alarms: state.storage,
blockConcurrencyWhile: (fn) => state.blockConcurrencyWhile(fn),
});
export class ActorHost {
async alarm() {
await reminders.onAlarm(); // fire what is due, re-arm the rest
}
}
The default shardedReminders() splits one table into fixed hash shards and polls it, because
a host holds many actors and has to find whose reminder is due. A DO holds exactly one, so
there is nothing to search and nothing to poll — reminders live in the object's own storage and
the platform wakes it at the earliest due time.
The visible consequence: an alarm fires at the due time, where shardedReminders()
promises only "at or after nextDue".
A dispatch that fails — a deadline, an onReminder that threw — is re-armed one
reminderTickMs out and counted in HostStats.remindersUndelivered, so a failing target costs
at most one attempt per tick. The rules for a reminder the actor set or cleared meanwhile are
the shared ones.
Client sockets
Browsers can reach actors over a WebSocket here, terminating in either half:
createWorkerHandler({ ..., socket: {} }); // in the Worker
createWorkerHandler({ ..., socket: { terminate: 'object' } }); // in the object
Worker-terminated gives one multiplexed socket per client, reaching every actor — the
right shape for a dashboard. Object-terminated gives one socket per actor, accepted with
the hibernation API inside the object that owns it — the room pattern, and the mode where a
disconnect actually releases the activation, because teardown happens locally instead of dying
at the stub.fetch boundary.
Pair either with socketTransport() on the client. The
deployment guide has the choice in full,
plus the hibernation contract.
The getting-started path
Most apps do not touch the two seams directly — createHostDurableObject() and
createWorkerHandler() assemble them. See
Cloudflare Workers for the full entry, the wrangler.jsonc
and the three gotchas that are each worth their own callout.
Next steps
- Installation — bindings and migrations.
- API reference — exports.
- Cloudflare Workers — the deployment guide.
@sigx/actors-ws— the client half of a socket.
