Skip to content
downpipes docs

Backing up your Cloudflare data with downpipes

A downpipe is the unit of work in this product. It pairs one source (a KV namespace, an R2 bucket, a D1 database, a Secrets Store, a set of Workers, a Stream video inventory, a Cloudflare Images inventory, or a slice of your Cloudflare configuration) with one or more destinations, and it captures that source on a schedule. The engine runs entirely inside your own Cloudflare account. It writes the bytes only to destinations you control, and the vendor holds nothing.

This page is for the operator running downpipes. It gives you the mental model for what happens when a backup runs, end to end. It points you at the deeper pages for each step. It does not cover restoring or recovering data. That is the job of the Recovery section, and a backup that has never been proven recoverable is only half the story, so read both.

Nothing here is instant. Every automatic backup, every failover, and every replica copy is driven by a scheduled pass that fires on a fixed interval, so the practical floor on how fresh a scheduled backup can be is about fifteen minutes. The rest of this page explains why, and what that means for your recovery point.

What a downpipe is

A downpipe holds a few pieces of configuration that together decide what gets backed up, where it goes, and how often. The most important ones are below.

FieldWhat it controls
nameA human label for the downpipe, 1 to 256 characters. The downpipe id is derived from it.
sourceThe single Cloudflare resource this downpipe captures (one source per downpipe).
cadenceSecondsThe interval between runs, as a whole number of seconds. The minimum the engine accepts is 60, but the console floors the schedule picker higher (see below).
destinationIdsThe ordered list of destinations a run fans out to. Index 0 is the primary the run seals to. The rest are replicas. Absent means the account default, a single copy.
retentionAn optional per-downpipe history policy (keepRuns and/or keepDays). Absent keeps every run. Deletion only ever happens when you turn enforcement on.
restoreTestCadenceSecondsHow often the engine drills the latest run to prove it restores. Off keeps backups running but stops the scheduled proof.

A downpipe captures exactly one source. If you want to back up three KV namespaces and an R2 bucket, that is four downpipes, each on its own cadence. You never run a terminal command or a script to trigger a backup. The engine’s scheduled pass does the automatic dispatching, and you can also trigger a run on demand from the console with Run now.

How a run actually happens

A single run moves through four stages. They do not all happen at once, and the last one happens in a separate scheduled pass rather than during the run itself.

  1. Capture the source

    The engine reads the source inside your account and assembles the records for this run. A run that is too large for one invocation’s CPU and subrequest budget continues across alarm-driven slices until it finishes, so a big source does not have to complete in a single tick.

  2. Seal to the first reachable destination

    The run is encrypted, hashed, signed, and written to a destination, along with that destination’s own RUNLOG chain entry. A downpipe that fans out to two or more destinations seals to the first reachable one in order: the configured primary, then each replica. If the primary is down, the run still lands on a healthy replica rather than failing.

  3. Read it back and verify before reporting success

    Right after the seal and before the run counts as a clean success, the engine reads the just-written archive back from the destination and verifies it. A corrupt or partial backup is caught now, at seal time, not at the next restore test up to a week later. The verdict is attached to the run. The check never deletes or fails the run, so a suspect verdict is a loud signal to investigate, never a destructive action.

  4. Replicate to the other destinations on a later pass

    A separate scheduled pass copies each finalised run from the destination it sealed to (its recorded origin) onto every other configured destination, catching each one up on its whole backlog. Replication runs after the seal so a replica copy never delays a backup, which is why a fresh fan-out run legitimately shows fewer than its full copy count for a short while.

The order is fixed. A run seals to exactly one destination and is read back and verified there, then the copies fan out afterwards. A run never writes to every destination at the same instant.

The fifteen-minute floor

There is one scheduled pass that drives everything automatic: a cron trigger that fires every fifteen minutes (the */15 * * * * schedule in the engine’s wrangler.toml). That single tick is the only scheduled dispatcher: it dispatches due runs, runs scheduled restore tests, computes retention plans, flies the canary, and runs the replication backlog. A console Run now (POST /admin/trigger) is the separate on-demand path: it dispatches and seals a single run immediately, outside the schedule.

A downpipe whose cadence has elapsed runs at the next tick. So a downpipe set to “hourly” does not fire at exactly the top of the hour; it fires at a fifteen-minute tick. The seal itself runs outside the scheduler Durable Object (the Durable Object owns the schedule, the run lock, and the RUNLOG index, but never the seal), and a large run continues on its own Durable Object’s alarms between ticks.

The cadence is a ceiling, and this section’s floor is a different one

Two different floors meet on this page, and they are easy to conflate. The fifteen-minute tick is a floor: nothing scheduled dispatches more often than that, so it bounds how fresh a backup can be. The cadence you pick is a ceiling on the due time, not a floor: the next run falls due no later than one cadence after the previous run completes. The run normally starts at the first tick at or after that due time, up to fifteen minutes after it. A tick that runs low on budget carries its remaining due runs to the next tick.

The mechanism is a deliberate backwards jitter. When a run completes, the engine sets the next due time to the completion instant plus the cadence. It then subtracts a random amount of up to a tenth of the cadence (JITTER_FRACTION = 0.1, jitteredCadence, engine/src/sched/schedule-window.ts). Subtracting spreads a fleet of same-cadence downpipes so they do not all seal on the same tick, and it subtracts rather than adds for a reason worth stating: a cadence on a backup is a promise about how stale your newest archive may get, so overshooting is the harmful direction. Adding the offset instead would make a “daily” downpipe run every 24 to 26.4 hours and breach the interval its operator asked for. The cost of subtracting is that a run starts slightly sooner and does slightly more work.

So the gap from one run’s completion to the next due time falls in a band, and the cadence is its upper end.

PresetCadenceGap to the next due timeSame in words
Hourly3600s54 to 60 minutes due, though the tick absorbs itsix minutes of jitter is smaller than the gap between two ticks, so an hourly downpipe still fires an hour or an hour and a quarter apart
Daily86400s21.6 to 24 hoursevery day, sometimes as much as 2.4 hours early, and normally up to one tick late
Weekly604800s151.2 to 168 hoursevery week, sometimes as much as 16.8 hours early, and normally up to one tick late

Read the band, plus one tick, as the normal case. A daily downpipe falls due 21.6 to 24 hours after its previous run completes, and a weekly one 151.2 to 168 hours after. Size a retention window or a compliance interval against the upper end plus fifteen minutes, with a margin for a crowded tick; size a cost or quota projection against the lower end, because that is where the run count peaks.

Deriving a run count from the nominal cadence under-counts

Dividing an elapsed window by the nominal cadence gives the minimum number of runs, not the expected one. A healthy downpipe over-delivers against it: about 31.4 runs where a nominal daily figure says 30 over thirty days, and up to a ninth more (11.1 per cent) in the worst case. That is correct behaviour, not drift.

It is also why the signed SLA compliance report can show successful runs exceeding expected runs: that is a pass, not an arithmetic fault. Do not reach for the apparent fix of dividing by the reduced figure instead: that would set the bar at roughly 33 runs for a nominal 30 and turn every compliant downpipe into a reported breach.

The jitter applies only to the plain cadence path. An advanced schedule with a cron expression computes its next fire from the cron and the time zone and is not jittered, because an operator who wrote “fire at 02:00” meant that minute, and a tenth of a daily cadence would be hours away from it (nextWithJitter, engine/src/sched/scheduler-do-scheduling.ts). If you need a run at a fixed instant rather than inside a band, use a cron expression rather than a preset.

The practical consequence is your recovery point. The freshest a backup can be is bounded by the tick, so the smallest meaningful gap between “now” and “the newest good run” is about fifteen minutes. The console reflects this: the schedule picker’s fastest preset is Hourly, and an advanced cron expression still dispatches on the same fifteen-minute pass, so a cron set for :07 lands at the next tick after :07.

Cadence is a single interval per downpipe. There are no backup windows, no blackout periods on the simple path, and no per-run cron expressions in the default picker. An advanced schedule can add a cron expression, a time zone, and maintenance blackout windows that defer a fire, but the dispatch still rides the fifteen-minute tick. The cron expression is a standard five-field crontab expression (minute, hour, day-of-month, month, day-of-week). Month or day names such as JAN or MON, @-macros such as @daily, and a seconds field are all rejected. The time zone must be a zone name the engine accepts, such as Australia/Sydney. Matching is case-insensitive, so australia/sydney resolves the same as Australia/Sydney. A fixed UTC offset such as +10:00 is accepted too, but it does not follow daylight saving, so name the IANA zone. Left blank, it defaults to UTC. Treat the interval as a ceiling rather than a floor or a guaranteed firing time: the cadence path subtracts up to a tenth of the cadence as spreading jitter, so a run falls due inside a band that ends at the cadence, then starts at the next tick, as set out above.

Maintenance blackout windows

The advanced schedule can carry maintenance blackout windows: spans of local time during which a scheduled run is held back. This is the console’s Use a cron schedule and/or maintenance windows option; ticking it reveals the cron, time zone and window fields.

A blackout window never drops a backup, it only delays one. A fire that would land inside an active window is deferred to the moment the window closes, then runs on the next fifteen-minute tick. So a window is a “not during these hours” instruction, not a “skip this run” one, and your history stays complete. Back-to-back windows defer in turn, up to an internal bound, so a fire cannot be pushed indefinitely.

From engine 0.3.6 the engine checks the windows again when a tick starts the run. If a run falls due just before a window opens, the engine moves the run to the end of the window. It does not start the run at the first tick inside the window. The same check holds back a run when you add a window after the engine set the next run time.

The engine moves a run in this way one time only for each due time. If no tick falls in the gap after the windows, the run starts at the next tick, even inside a window. If the windows cover all of the day, the run starts on time.

Each window has a set of weekdays and a start and end time.

  • Weekdays. Tick the days the window applies to. Leaving every day unticked applies the window every day, rather than none.
  • Start (24h) and End (24h) are local times as HH:MM on a 24-hour clock, for example 02:00 and 06:00. They are read in the schedule’s time zone, the same zone as the cron expression (UTC when you leave the zone blank), so a window follows your wall clock across daylight saving.
  • The window is start-inclusive and end-exclusive: 02:00 to 06:00 covers 02:00 up to but not including 06:00.
  • An end earlier than the start wraps past midnight, so 22:00 to 06:00 is the overnight span. The End field does not accept 24:00. To run a window to midnight, set the end to 00:00, so 22:00 to 00:00 covers 22:00 up to midnight.
  • A window whose start equals its end is refused: it would cover no time, so the save is blocked until you change one of the times. A window row with both times blank is ignored; a row with only one of the two times set is likewise flagged and blocks the save until you complete or clear it.

A blackout window only shifts when a run fires; it does not change the cadence, the retention policy, or anything a run does once it starts.

Where backups go

Destinations are your own archive targets: an R2 bucket, an S3-compatible store, a Google Cloud Storage bucket or an Azure Blob container that you control. Each destination is a self-contained archive with its own RUNLOG, so it is an independent, restorable mirror rather than a shard of one logical store. That independence is what makes redundancy real: losing one destination does not corrupt another. Where the four providers differ, one row each, is destination providers compared.

The setup order is destinations first, then downpipes. A downpipe can only fan out to destinations that already exist, and the console verifies a destination with a write probe at save time. Attach two or more destinations to a downpipe to get redundant off-source copies. A single-destination downpipe has no redundancy concept and shows no copy-count readout.

How long history is kept

By default, downpipes keeps every run forever. History is only ever pruned when you set a per-downpipe retention policy, and even then nothing is deleted until you explicitly turn on enforcement.

A retention policy bounds history by keepRuns (the most recent N runs), by keepDays (runs within the last N days), or by both. Setting a policy without enforcement is a dry run: the engine computes and logs what it would prune and deletes nothing. Only the off-by-default enforcement gate makes the engine delete, and even then a pruned run’s RUNLOG entry is kept and marked superseded rather than removed. Retention and pruning is the single path in the whole product that deletes archive bytes, so it has its own page and its own loud warnings.

Where this fits

For where the engine and its scheduled pass sit in your account, see the deployment topology. To read the run history a recovery picks from, see runs and history.

Last updated .