Recover your data: the restore flow from dry-run plan to receipt
This is the hero flow of downpipes, walked end to end. It is written for an approver or a recovery owner who is about to run a real restore and wants to know exactly what each step does and what it costs. Everything up to the apply is read-only; the apply is the one act that writes archived data back into your live account, and the engine does not roll it back.
The flow is the same whether you start from the Runs activity, a deep link to a specific run, the restore screen’s own picker, or its calendar. You always restore from a run identified by its id: the calendar marks the days that hold one and lists that day’s times, but choosing one still resolves to a specific run, never an arbitrary-time cursor. The portal builds the recent-run history as a bounded ring of the most recent runs (fifty of them), which the picker and the calendar both draw from, so you can pick one at a glance, but you can also restore from any run id directly, including one that has rolled off the ring.
The flow opens on a form that is pure input: you name the run and choose the blast radius, and nothing is built until you ask for the plan.

A cold arrival at the restore screen does not ask you for the run id first. The card opens by saying you do not need it and offers two buttons, one to pick from your recent runs and one to browse by the date you want to go back to; the run id field sits beneath them under “Already have the run id?” for the case where you do. Arriving with a run already in hand, from a deep link or a retry, puts the field back on top, because there it carries the answer rather than asking the question. That second arrival is what the capture above shows, with the field leading and the browse row beneath it. The picker lists your recent sealed runs to choose from.

A restore cannot be undone
Once an apply begins writing, there is no rollback. A cancel stops further writes but does not undo records already written. Read the dry-run plan and the blast-radius cues carefully before you arm an apply, and prefer restoring into a fresh empty target where you can.
The journey
Read the dry-run plan
Open a run and the engine returns a
RestorePlan. Withconfirmabsent or false it writes nothing. The plan reports the records it verified read-only, the planned writes and their upper-bound byte total, a resolved-destinations sample of where each record would land (capped at fifty rows), and any records it skipped with an honest reason. The dry-run branch is open to any authenticated role, including Viewer, so anyone can read the full preview; onlyconfirm: trueis capability-gated. A skipped record is never silently dropped: a secrets record, a Workers script, or an out-of-band Cloudflare config surface appears in the skipped list with guidance.Read the blast-radius cues
The console scales the plan’s impact into plain language. It distinguishes the latest run from an older one, shows the planned writes and bytes, and flags whether records go back to their original binding or are redirected to a different one. Every one of these figures is recomputed by the engine at request time from the run and the selectors; none is trusted from the client. A direct API caller that tries to seed a misleadingly benign figure is ignored and the divergence is logged. So what an approver sees is the engine’s own view of the run, not whatever the requester claimed.
Raise a dual-control request
To move from preview to a real apply you raise a request bound to the exact plan, with a reason for the change. The reason is mandatory and is recorded for the trail. The request carries no authority to write on its own; it simply puts the plan in front of an approver. The bare shared admin token cannot raise a request, because it has no stable subject and dual control needs an attributable identity. The plan the approval keys on is a hash the engine recomputes server-side, so the binding is the engine’s, never a value the client chose.
A distinct approver approves
A second authorised person approves that exact plan. The approver must differ from the requester on a stable identity, and the engine refuses a self-approval at approve time. This is the canonical dual-control mechanic; its full detail, including the maker-is-not-checker rule, the plan-hash binding and the single-use, twenty-four-hour expiry, lives on dual control. Approving from a passkey or recovery-code cookie session now also requires a fresh passkey step-up re-authentication, meaning a sign-in within the last five minutes or a fresh single-use step-up assertion; the console runs the assertion and retries automatically, and a bare break-glass token session and a Cloudflare Access session are exempt. You do not need to re-derive it here: the practical effect is that the Apply button stays locked until a distinct approver has signed this plan, and the console polls so it unlocks shortly after they do.
Arm and apply
With an approval in place, arm the apply. The engine then gates it server-side in a fixed order, and each gate has a reason for sitting where it does.
First the apply capability: an unauthorised apply is refused with a 403 before the rate limiter runs, so a blocked production write is never masked by a 429 and is always audited. Then a fresh passkey step-up, on the same terms as the approve, so a session left open cannot write your data back; it is asked for before the rate limiter and before any approval is touched, and the read-only dry-run in the same request is deliberately not gated, so the prompt appears on the confirmed apply only. Then a usable approval bound to the exact plan hash with a distinct approver; with none, the apply is a 403 “restore not approved” that the console renders as an awaiting-approval state, not a mystery failure. Then, if Require Change Number is on, a valid change reference, enforced after the approval gate so that only an otherwise-authorised apply raises a change record. Then the engine’s two-phase integrity verify, described next. Only after all of them pass does any byte get written.
Trust the integrity safety net
The apply never writes an unverified byte. In phase one the engine verifies every in-scope record’s plaintext hash, read-only, with nothing written; a single failure aborts the whole apply with zero records restored, so a tampered archive can never half-overwrite a live resource. In phase two the engine re-verifies each record immediately before it writes that record, materialising one value at a time. A re-verify failure mid-apply, which could only mean the immutable archive changed underneath the restore, stops it as an integrity failure.
Read the receipt
On completion the receipt records the result honestly. A clean apply states how many of the verified records were written and how many bytes. The receipt names both identities: the applier (or, on the shared-token fallback, an explicit “no attributable identity” statement rather than an omission) and the distinct approver who signed the plan. A partial apply is never dressed up as success: it states plainly how many records were written and how many could not be, lists the per-record reasons, and offers to retry just the failed subset under a fresh approval. That holds for Cloudflare config surfaces as well as for records. A surface your edit token is too narrow for is refused item by item rather than aborting the whole apply, and the surface is then raised as a failure of its own carrying the count per refusal class and the remedy, so the receipt records it with
verified: falseand the result is not reported as complete. The apply’s per-surface outcomes still carry the finer, item-level detail, and the restore receipt sets out exactly what the receipt does and does not carry.
Proportional friction
The confirm step is calibrated to the blast radius rather than fixed, so a routine restore is not buried under ceremony while a dangerous one demands attention. A small restore that writes records back to their original binding from the latest run uses a single confirm. A restore that redirects records to a different binding, or restores an older run over current data, escalates to type-to-confirm, where you type the run id (or the target binding name on a redirect) to proceed.
| Restore shape | Confirm friction |
|---|---|
| Same binding, latest run, small | A single confirm modal |
| Redirected to a different binding | Type-to-confirm |
| An older, non-latest run | Type-to-confirm, with an explicit “restores older data over current data” note |
The in-account restore this flow drives is bounded to 200 records, so there is no separate large-restore tier reached by sheer write count; a run larger than that is recovered offline rather than in the console, as the maxRecords cap explains.
Whatever the friction, the confirm dialog states the irreversibility in the same words: a cancel stops further writes but does not undo records already written, and the engine does not roll back. The threshold and the escalation are presentation; the security gates above (role, approval, integrity) are the engine and are absolute regardless of which confirm path you see.
The approval is single-use
The approval that unlocked your apply is consumed only after a successful apply, in one atomic step. A failed apply leaves it usable, so you can retry the same plan without a fresh round of dual control. A second apply against a consumed approval is refused. Approvals expire twenty-four hours after they are raised, so an approval cannot be banked indefinitely against some future apply. The mechanics of consumption and expiry are detailed on dual control.
What the apply writes back
KV, R2 and D1 records are written back in account after the verify. Some classes are handled out of band by design: Secrets Store values are never restored in account, Workers scripts are reprovisioned with guidance rather than blind-redeployed, and only a few idempotent Cloudflare config surfaces re-apply in console. Those out-of-band items are reported as skipped with guidance and do not, on their own, make a restore fail. The full breakdown is on what restore can and cannot write back.
Why role alone is never enough to apply
The apply route checks the apply capability first, but holding the role is never sufficient on its own. After the role gate the router asks the scheduler Durable Object, read-only, whether a usable approval exists for this exact plan hash before touching live data. A usable approval is one whose effective status is approved, with a recorded approver subject that differs from the requester subject. With none, the apply is refused with an explicit “restore not approved” outcome that carries the plan hash. The role gate and the dual-control gate are independent and both mandatory, so a restore-operator or approver acting alone, with no second identity’s approval, cannot write. The receipt then records the applier as the actor and the approver carried from the consumed approval, on both the stable-subject axis and the display-email axis. Any cryptographic verification of a receipt is done out of band; the console does not verify receipts in the browser. The audit event for the apply carries operator identity by design, including emails and source IPs.
Restoring several downpipes at once (batch restore)
When you need to bring several downpipes back in one sitting, the batch restore queue runs them together. You reach it from the Downpipes list: select the rows you want, then choose “Restore…” in the bulk bar. Each selected downpipe becomes a row that restores its own latest run.
The important property is that batch mode batches the console only. It is a queue over the same per-run dry-run, request, approve and apply path this page describes, with no bulk restore route behind it. Every row keeps its own dry-run plan, its own dual-control request and its own approval by a distinct approver, and its own apply. Approving one row never arms another, two rows never share a plan hash, and the maker-is-not-checker rule holds on every row exactly as it does for a single run. A single reason field fills each request, and “Request approval for all planned rows” is a convenience that fires one independent request per planned row rather than one approval spanning many.
The scope is deliberately narrow. Each row restores that downpipe’s latest run to its original bindings only. A redirect, a single record, a record cap, Cloudflare-config, media, or a D1 table-subset stay on the single-run flow, which you reach for any row through “Restore this one individually”. There is deliberately no “apply all” button either: every apply is its own click, gated on that row’s own usable approval, with its own confirm friction if the run is not the latest. A row shows as blocked, with the reason stated, when the downpipe has no completed run yet, when it was deleted between selection and arriving here, or when its dry run could not build a plan.
Where this fits
- Dual control is the canonical home for the approval mechanic this flow depends on.
- What restore can and cannot write back sets expectations for the in-band and out-of-band data classes.
- Proving recoverability covers the read-only verbs you can run before ever committing to an apply.
- Roles and capabilities defines the request, approve and apply capabilities and which role holds each.
- The restore and recovery API documents the underlying engine routes.
Last updated .