Engine architecture: the Worker, the three Durable Objects, and the cron that drives everything
This page is the conceptual model of the downpipes engine: the single Worker, the three Durable Objects it runs, and the scheduling truth that follows from how they are wired. It is written for a developer or an architect who wants to know what dispatches a backup, what carries a large run across the platform’s invocation limits, and why a backup cannot run on a schedule more often than roughly every fifteen minutes.
The engine deploys entirely inside your own Cloudflare account. In the default topology it has no public hostname at all. The model is small on purpose: one Worker holds the request and cron entry points, one Durable Object is the authority plane that owns the schedule and the run lock, and a second, per-downpipe Durable Object carries any run too large for a single invocation. A third Durable Object class, the account-global rate-limit object, backs a distributed Cloudflare-API pacer. Its binding is declared in the shipped wrangler.toml and its class carries a migration, so it is present on the default path rather than something you add; what is deferred is the live cross-isolate behaviour, and the engine falls back to a per-isolate pacer when the binding is absent. The scheduling and sealing model below involves only the first two objects. The scale ceilings, the per-invocation budget arithmetic and the source-size limits live on scale and limits; this page owns the model and the floor.
The three moving parts
There are exactly three runtime components, and it helps to name what each one is and is not responsible for.
| Component | Kind | Owns | Source |
|---|---|---|---|
| The engine Worker | Cloudflare Worker | the admin API (fetch) and the cron entry point (scheduled); it also runs the seal during an invocation |
engine/src/index.ts |
SchedulerDO |
singleton Durable Object | schedules, the monotonic RUNLOG index, the in-flight lease, run history, roles, audit, the config-version chain | engine/src/sched/scheduler-do.ts |
RunSealDO |
per-downpipe Durable Object | carrying one large run across invocations on a durable-alarm chain | engine/src/seal/runstate.ts |
RateLimitDO |
singleton Durable Object | the account-global token bucket that paces Cloudflare API crawls in aggregate, so concurrent crawls stay under the account limit rather than each isolate pacing itself | engine/src/sched/ratelimit-do.ts |
The Worker has two entry points. Its fetch handler serves the in-account admin API that the console calls, and its scheduled handler is the cron driver. Both are declared in one file (engine/src/index.ts). The seal itself, the heavy work of crawling a source, encrypting every byte and writing the archive, runs inside a Worker invocation or inside RunSealDO. It never runs inside SchedulerDO.
The scheduler Durable Object is the authority plane, not a worker
SchedulerDO is a single object per account, and it is deliberately the cheap, serial place where all the authority lives. It holds the schedule for every downpipe, allocates the account-global RUNLOG index that orders runs, tracks the in-flight lease that stops a downpipe being run twice at once, and keeps the run history, the roles, the audit log and the config-version chain (engine/src/sched/scheduler-do.ts).
Because so much depends on it being responsive, it does no heavy lifting. The design decision recorded in the code as F11 is that the seal is dispatched out of the scheduler, to a Worker invocation or a child Durable Object, so the authority plane stays quick to answer. A crawl that hashed and encrypted gigabytes inside the scheduler would block every schedule read and lease check behind it.
The scheduler does keep a Durable Object alarm, and it is easy to assume that alarm dispatches runs. It does not. The alarm only re-arms itself for the next due time (rearmAlarm, engine/src/sched/scheduler-do.ts). When the alarm fires it does a little opportunistic housekeeping and then computes the earliest next due time across all enabled downpipes and sets the next wakeup. It never invokes the seal. The code is explicit that precise alarm-driven dispatch, where the alarm itself drives a seal, is a deferred refinement that would need the engine wired to itself as a service binding so the alarm callback could seal. That refinement is not built, so scheduling precision is bounded by the cron interval.
The alarm re-arms; it does not dispatch
The scheduler alarm exists so the object has a durable next-wakeup and can self-heal a stale state. It computes and sets the next alarm time and nothing more. The only thing that starts a backup automatically is the cron tick described next; the alarm never does. An operator can also start a single run on demand from the console with Run now, which dispatches outside the schedule. Treat the alarm as bookkeeping, not as a scheduler that runs jobs.
The cron tick is the only scheduled dispatcher of a backup
The Worker’s scheduled handler runs on a */15 * * * * cron, declared in engine/wrangler.toml. That tick is the sole scheduled driver of new runs. On each tick the handler calls a single drive pass that asks the scheduler for the set of due downpipes and then runs each one out of the scheduler with a trigger, a seal, and a completion (drive, engine/src/cron/drive.ts, wired from engine/src/index.ts). An operator can also start a single run on demand from the console with Run now (POST /admin/trigger, engine/src/admin/router.ts), which dispatches and seals one run immediately rather than waiting for a tick. The cron is the automatic path; Run now is the manual one.
The sequence per tick is plain. The driver reconciles the schedule, reads the due set, orders it oldest-due-first for fairness, and for each due downpipe triggers a run, drives the first slice of the seal inline, and posts the completion. After the run loop it does the lower-priority passes: alert and digest delivery, the credential-expiry check, scheduled restore tests, retention pruning, replication fan-out, and the update-channel check. Every one of those tail passes runs after the runs and is fail-open, so none of them can delay or crash a backup.
The consequence is the part to internalise. A downpipe’s requested cadence is stored as a number of seconds, but the effective cadence is the cron tick. A cadence set below the tick interval does not run sooner; the next run is simply picked up at the next */15 tick. The code states this directly on the cadence field: the effective floor is the engine cron tick, so a smaller value dispatches at the next tick, not sooner (the cadenceSeconds field, engine/src/sched/types.ts). In the console, the schedule picker floors at Hourly, so an operator is never offered a sub-tick cadence in the first place.
The fifteen-minute effective backup floor
Putting the two previous facts together gives the headline number. The cron fires every fifteen minutes, and the cron is the only scheduled dispatcher, so a scheduled backup runs at most about once every fifteen minutes no matter what cadence is configured. This is the effective backup floor, and it is the practical lower bound on how fresh an automatic recovery point can be. A console Run now can start a run between ticks, but it is a manual action, not a way to set a faster schedule.
This floor is an RPO-granularity caveat, not a defect. It is cross-linked from the places where freshness matters: it shapes the recovery point objective on objectives, RPO and RTO, and it is the reason the backing-up overview describes a recovery point as the newest good run rather than an arbitrary moment in time. downpipes does not offer, and this page does not imply, sub-fifteen-minute backups.
Do not advertise or design for sub-fifteen-minute backups
The */15 cron tick is the floor. The scheduler alarm does not shorten it, and a smaller configured cadence does not either. If a workload genuinely needs a tighter recovery point than this, downpipes is not the mechanism for that part of the workload; size your recovery point objective around the newest good run, never around a clock you can pick a minute from.
How a large run survives a single invocation
A Worker invocation has a finite budget: a per-invocation subrequest cap and a CPU and wall-clock ceiling. A small backup completes inside the tick’s first inline slice and never touches the seal Durable Object, behaving exactly as a single-pass seal would. A large backup cannot finish in one invocation, so it is handed to RunSealDO (sealRunSliced, engine/src/seal/runstate.ts).
RunSealDO is a per-downpipe object that chains the run across alarms. Each alarm invocation has its own fresh platform budget, which is precisely why the run is advanced through alarms rather than looped inside one invocation that would hit the cap. On each alarm the object heartbeats the scheduler’s in-flight lease so a long run is never reclaimed as crashed mid-flight, seals one budget’s worth of work, persists the advanced checkpoint and that slice’s output atomically, and re-arms the next alarm. When the source is exhausted and there is headroom, it finalises the run, reads the archive back to verify it, and posts the completion (alarm, engine/src/seal/runstate.ts).
Two correctness properties are worth calling out. The alarms are durable, so a slice that crashes resumes from its last checkpoint rather than restarting the whole run, and content addressing makes the re-seal idempotent. And the destination chosen when the run started is pinned for the life of the run, so a mid-run destination change can never split one archive across two stores. The exact budget figures, the finalise reserve and the tuning knobs that govern slicing are on scale and limits.
The seal Durable Object alarm only carries an already-started run
There are two alarm chains in the engine and they do different jobs. The scheduler alarm only re-arms a wakeup and never dispatches. The seal alarm only carries a run that has already been started from one slice to the next. Neither alarm begins a backup: a run is begun either by the */15 cron tick automatically or by a console Run now on demand, and the seal alarm then carries whichever one it is across slices.
The engine is routeless, and the console reaches it over a service binding
In the default topology the engine Worker has no public hostname. The wrangler.toml disables the workers.dev route and declares no custom-domain route, so the cron triggers and the Durable Objects run without any public surface at all (engine/wrangler.toml). The only public domain in a default deployment is the console’s.
The console reaches the engine over a worker-to-worker service binding, not a public URL, and proxies the admin and support paths to it same-origin. A split topology exists for deployments that want to front the two surfaces with separate Access policies, and that mode gives the engine its own custom domain, never a workers.dev host. Either way the engine is administered through the console, and the console origin is the value the engine allowlists for cross origin requests.
Where this fits
This page is the runtime model. For what one run actually does from crawl to sealed archive, see the anatomy of a backup run. The scale ceilings, the per-invocation subrequest budget and the source-size limits that this page deliberately leaves out are on scale and limits. The freshness consequence of the floor is developed on objectives, RPO and RTO, and the deployment topology, the service binding and the custom-domain rule are covered in full on topology.
Next steps
To stand the engine up and watch the cron tick land its first run, follow first run and setup. To understand how the schedule, the lease and the config-version chain are administered once the engine is running, see change control.
Deeper detail: the lease, the budget split and the topology toggle
The in-flight lease. A downpipe whose previous run is still in flight is coalesced rather than queued twice. The lease bounds a crashed holder: an in-flight run past the lease window is treated as crashed and reclaimable, so a downpipe self-heals within roughly two cron intervals instead of wedging forever, while a genuinely running seal is never reclaimed (engine/src/sched/scheduler-do.ts). The seal Durable Object heartbeats this lease on every alarm so a long, healthy run keeps its claim.
One budget for the whole cron invocation. A tick can have many due downpipes, and they all share one platform subrequest cap, so the driver spends one budget across them: it reserves a fixed overhead for the scheduler round-trips and the tail passes, spends a slice on each due downpipe, and stops dispatching new downpipes cleanly while budget remains rather than being killed mid-loop at the cap. Undispatched downpipes stay due and are picked up on the next tick. The figures behind this are on scale and limits.
Why the seal stays out of the scheduler (F11). The scheduler is the serial authority plane every schedule read, lease check and RUNLOG allocation passes through. Running a multi-gigabyte crawl inside it would serialise all of that behind one slow run. So the heavy work runs in the Worker invocation or the per-downpipe seal object, and the scheduler stays a quick, contended-but-cheap coordinator (engine/src/seal/runstate.ts).
The topology toggle. The default is routeless: no workers.dev, no custom-domain route, the console proxies same-origin over a service binding. The split topology, present as a commented route in wrangler.toml, gives the engine its own custom domain for deployments that want per-hostname Access policies. The split mode never uses a workers.dev host either; custom domains only (engine/wrangler.toml).
Last updated .