Shipped as option 1 (parent-owned alarm):
setAlarm/getAlarmreserved verbs on the worker capability;StatefulWorkerDurableObject's real platform alarm IS the facet's alarm (the only extra state is the worker recipe to boot a cold fire), fires are delivered through the worker'sinvokeCapabilitydispatcher (workerd reservesalarmas an RPC name) and rethrow into native retry;IterateDurableObjectpresents the standardctx.storagealarm API automatically (the host delivers the worker's own ref on first contact); the template guestbook registers with{ recovery: true }, passing its own build identity (env.ITERATE_WORKER_VERSION) as the registry version so the crash-loop budget's deploy-reset lane works.
Facet alarms: parent-owned alarm proxy so userspace processors get keepalive recovery
Userspace stream processors (PR #2073's guestbook shape: a StreamProcessor
hosted in a project worker's IterateDurableObject behind a native wake
subscription) cannot opt into { recovery: true } today: the keepalive arms
durable alarms, stateful dynamic workers are hosted as workerd FACETS inside
StatefulWorkerDurableObject, and facet storage has no alarms. Every
delivery then fails with "alarms are not yet implemented for SQLite-backed
Durable Objects" — proven live on PR #2073 (the recovery wiring was written,
failed on first delivery, and reverted with the finding documented at the
registration site in configs/default/worker.ts). Until fixed,
userspace processors are limited to idempotent at-head appends; consequential
background obligations need a platform-hosted DO.
Ground truth (workerd source + Cloudflare, researched 2026-07-17)#
- The throw is workerd's DEFAULT
ActorSqlite::Hooks::scheduleRun()(src/workerd/io/actor-sqlite.c++~1214). Inserver.c++(~1199), actors WITH a parent — every facet — always get the throwing default hooks:// TODO(someday): Support alarms in facets, somehow.Still on workerd main as of 2026-07-17. Root actors get realActorSqliteHookswired to theAlarmScheduler. - Structural, not a flag:
AlarmSchedulerkeys alarms by{uniqueKey, actorId}— no facet dimension exists, andFacetManagerexposes nothing alarm-adjacent. Not implemented in production either: tracking issue workerd#6810 (open, unlabeled) has one Cloudflare reply — "Facets currently don't support alarms. In agents, we made it so calling.scheduleinside a subagent proxies up to the parent." Their agents SDK does exactly that (root DO owns the one real alarm + schedule table; facets RPC up; rootalarm()dials back into facets). - Nasty failure mode: the facet's
setAlarm()APPEARS to succeed (it only writes SQLite metadata); the hooks throw later on the commit path through the output gate, breaking unrelated in-flight RPCs on the actor. - Waiting for upstream is not a plan: 13 months of facets shipping without alarms, GA'd 2026-04 without them, no roadmap commitment anywhere.
What the keepalive actually requires of an alarm transport#
From stream-processor-keepalive.ts / durable-object-processor-durability.ts
(the machinery is transport-free by design — armAlarm is an injected seam;
the registry touches alarms only via ctx.storage.{get,set,delete}Alarm):
- Millisecond-precision arming. The backoff ladder's first rung is 10s
(
KEEPALIVE_ALARM_LEAD_MS); minute-granular transports are unacceptable. - At-least-once fire delivery to
handleAlarm. A LOST fire is unrecoverable (the KV record proves the desire; nothing re-checks it). Late (minutes), early, and duplicate fires are all tolerated — the keepalive self-gates on its persistedarmedAtMsand every path is idempotent. AlarmInvocationInfois informational only (spans); the durable revivals-counter mark is the enforcement.
Evaluated options#
- Parent-owned alarm via a reserved platform verb — RECOMMENDED.
StatefulWorkerDurableObjectis a root actor; its alarms work everywhere. AddsetAlarm(atMs | null)to the reserved dynamic-worker platform surface (precedent:killonDynamicWorkerRpcTarget; declared methods win over the dynamic-dispatch fallback). The facet dials it on ITS OWN ref —itx.workers.get(SELF_REF).setAlarm(...)through the ordinary ITX loopback; the template already exports its ref (GUESTBOOK_APP_REF). The parent stays dumb: persist one{atMs, ref}cell (the ref rides the call, solving fire-time "which recipe do I boot"), mirror it into the real alarm, and on fire replayinvokeCapability({ path: ["alarm"], args: [alarmInfo] })into the facet — the registry's existinghandleAlarmdoes all slice routing and re-arms through the same verb. Failures rethrow, so workerd's root-actor alarm retry covers delivery. Survives facet rebuilds and parent evictions (state in parent storage). Meets every requirement above. This is the Cloudflare-endorsed shape (agents SDK). - Registry alarm transport backed by the platform Scheduler — rejected. The keepalive arms/disarms on every delivery batch; journaling schedule events at that cadence is spam, script-execution routing is heavyweight for a 10s-lead watchdog, and the Scheduler can't address stateful refs today. Scheduler stays the tool for domain schedules, not incarnation keepalives.
- Wait for / patch upstream workerd — rejected. No roadmap; a real fix needs new addressing machinery (facet-dimension alarm keys + delivery path), not a carryable patch.
- Variants considered and folded in: a dedicated alarm-relay DO namespace is
option 1 with an extra DO the parent already is; a parent-side heartbeat
poll fails the 10s-precision requirement; injecting a parent binding into
the facet is blocked (
ctx.facets.getaccepts only{ class, id }— no props, and worker env is per-isolate, not per-DO).
Implementation sketch#
- Platform (~60 lines):
setAlarmonDynamicWorkerRpcTarget(stateful refs only) →StatefulWorkerDurableObjectpersists{atMs, ref}+ mirrors the real alarm;alarm()on the wrapper boots the facet from the stored ref and replays["alarm"]into it, rethrowing on failure. - SDK (~30 lines):
facetAlarmState(ctx, env, selfRef)initerate/sdk— aDurableObjectState-shaped shim whosestorage.kvstays the facet's own but whose alarm calls route to the parent. Zero registry changes: the template passes the shim tocreateStreamProcessorRegistryinstead ofthis.ctx. - Template: resurrect PR #2073's reverted recovery wiring (in git history —
commit c066130fa's parent has it):
{ recovery: true },alarm()→registry.handleAlarm, contract consuming the revival fact viaPLATFORM_STREAM_EVENTS(already published initerate/processorsfor exactly this). - Proof: the guestbook wake e2e already detects broken arming (it failed
instantly when facet arming broke); add one targeted e2e for the fire
path — arm a near-term alarm through the shim, assert the fire reaches
handleAlarm(observable as the revival fact when work was owed), plus the preview lane.
Related: tasks/agent-llm-deadline-alarm.md (platform-side alarm-slice
consumer), tasks/stream-processors-as-facets.md (asked "can a facet own a
stream subscription and alarms?" — answers now: subscription YES, proven on
PR #2073; alarms NO, this task).