Skip to content
downpipes docs

What restore can and cannot write back, and how to handle the rest

A restore in downpipes writes some data classes straight back into your live account and deliberately leaves others to be handled out of band. This page is for a self-hoster who needs to know, before an incident, exactly which is which and why, so the out-of-band cases are an expectation rather than a surprise mid-recovery.

The dividing line is whether the engine can safely write a record back at runtime through a binding it already holds. Where it can (Workers KV, R2 and D1), it does, after the integrity verify. Where a runtime write would be unsafe or impossible (Secrets Store values, Workers scripts, most Cloudflare config), the engine reports the record as skipped with guidance and does not pretend to have restored it.

No in-band restore is reversible

Everything written back in account is written without a rollback. Once an apply begins writing there is no undo; a cancel stops further writes but does not undo records already written. A partial apply is reported honestly with counts. Prefer restoring into a fresh empty target where the data class allows it.

What restores in band

KV, R2 and D1 records are restored in account. The engine writes verified plaintext back into the live resource only after its two-phase verify has passed, so no unverified byte is ever written. Each class is reconstructed at full fidelity rather than value-only: a KV record carries back its metadata and expiration, and an R2 object carries back its content type, cache headers and custom metadata.

Data class In-band restore Fidelity and notes
Workers KV Yes Value plus metadata and expiration; values are bounded to 25 MiB and written as a single whole-value put
R2 Yes Value plus HTTP and custom metadata; large objects streamed
D1 Yes, with caveats Schema re-created and rows re-inserted parameterised into a fresh database; see below

D1 is per-batch atomic, not whole-database transactional

D1 deserves its own note, because its atomicity is narrower than people assume. The engine replays a structured D1 backup back into a live database binding: it re-creates the schema, re-inserts every row with parameterised statements so a row value can never become SQL, then applies indexes, triggers and views after the rows are in.

The only transaction primitive a Worker has for D1 is a batch, which runs its statements as one implicit transaction. The engine submits statements in bounded batches of fifty. So atomicity is per batch, not across the whole restore. A schema with more tables than a batch holds, or a single table larger than a batch, spans several batches, and a fault part way through can leave the database partially loaded. That is precisely why the contract is to restore D1 into a fresh, empty database, and why a D1 write failure is reported with a reason that says so plainly: drop the target and retry into a fresh database. A failed D1 record does not carry the generic access-error reason an idempotent KV or R2 key would; it tells you the target may be inconsistent.

Because a D1 backup is a per-table sequence, you can restore a chosen subset of a database’s tables into a fresh database rather than the whole thing, and optionally create only those tables for a minimal extract. The dry-run flags a selected child table whose foreign-key parent is not in the selection, so you can widen the scope before you apply. See granular and targeted restores for the d1Tables field and its foreign-key lint.

Stream and Images media re-upload with an edit token

Captured Stream video and Images files are their own case, in between the automatic in-band classes above and the out-of-band ones below. Left alone they stay out of band, but a restore can re-upload them into your account when you supply an edit-scoped Cloudflare token for Stream and Images. An image keeps its original id, while a transcoded video gets a new uid that the receipt reports as an id map. See media restore for the token, the size limit and the per-type behaviour.

What is handled out of band

Three classes are not written back in account, each for a concrete reason.

Secrets Store values are never restored in band

A Secrets Store secret value is captured by a backup, sealed encrypted into the archive, but it is never restored in account. Cloudflare Secrets Store bindings are read-only at runtime, so there is no runtime write path for the engine to use, and the restore sink for secrets refuses a write rather than silently skipping it, so you are never told a secret was restored when it was not.

What a backup holds for a secret is its encrypted value together with its wiring. On a restore, a secrets record is surfaced as restore out of band, and the guidance tells you to re-apply the value, recovered from the archive, through the Cloudflare API or wrangler. Restoring a secret value is something you do deliberately, with the value you recover, not something the engine does for you.

Workers scripts are reprovision, not redeploy

The engine never blind-redeploys a Worker from a backup, because re-deploying stale code over a live service could brick it. Instead a Workers script record is verified recoverable (the blind restore test decrypts and hash-checks the code, the settings and the versions inventory) and then surfaced out of band with re-deploy guidance, so you re-deploy deliberately.

Workers record Out-of-band guidance
Script content Re-deploy the script code from the verified content snapshot, then apply its settings record
Settings Re-create the bindings and secrets from the verified settings snapshot; secret values were never captured, so the checklist lists their names and types to re-provision
Versions inventory Informational only; re-deploy from the script content record
Schedules Re-create the cron triggers from the verified schedules snapshot, so the re-deployed Worker keeps its schedule

There is no destructive Workers restore path. The snapshot proves the code is recoverable; you choose when and whether to re-deploy it.

Cloudflare config: a few surfaces re-apply

Cloudflare config is one registry of 313 surfaces, of which 60 auto-restore in band and 253 are backup-and-preview only. Restore reflects that split. The 60 in-band surfaces can re-apply in console when you supply a Cloudflare token, and they re-apply additively: the write reads live config, diffs the verified snapshot, and writes only the differing fields. Config that the live account has but the snapshot does not is reported as live-only and left in place, never blind-deleted. The other surfaces, the ordered ones (such as Access dependency order) and the reprovision ones (such as certificate private keys), stay out of band with tier-specific guidance.

cf-config restore is supervised-first, not yet on the live demo

The in-console Cloudflare config restore path is built and wired on main but is not deployed to the live demo. The account scope is guarded: the engine compares the account you supply against the signed origin recorded on the archive, warns on the dry run when they differ or the origin cannot be verified, and refuses the apply unless you type the target account id back. The zone is guarded the same way on a proven mismatch. That makes a cross-account write deliberate rather than silent, and it does not replace care, so treat the first live in-band config restore as supervised. See restore into a different Cloudflare account.

The dry-run diff and the apply need different token scopes. A dry-run only reads live config to compute the diff, so a read-scoped token is enough; an apply writes the differing fields, so it needs an edit-scoped token. The single console field is labelled an edit token because the apply is what it is there for, and the token is used once for the restore and is never stored or logged. The exact permissions this edit token needs, per in-band surface, are listed in Cloudflare config backup and restore. That is a different token from the deploy token in Cloudflare token scopes.

How out-of-band items affect the result

An out-of-band item is not a failure. Neither a secrets record nor a Workers script sets the restore result to failed on its own. They are reported as skipped with guidance, alongside any selector-excluded records, so nothing is silently dropped. A restore is successful when there are no write failures; the skipped list does not, by itself, make it fail.

The plain consequence: a run that contains KV, R2, D1, secrets and Workers records can apply successfully, write the KV, R2 and D1 records back in account, and report the secrets and Workers records as skipped-with-guidance, all in one honest receipt. A genuine failure is a record that verified but could not be written, and that is what sets the result to not-ok and lists a per-record reason.

Outcome Sets the restore to failed Where it appears
KV / R2 / D1 written back No, it succeeded Records restored, with bytes
Secrets record No Skipped, with re-provision guidance
Workers script No Skipped, with re-deploy guidance
Out-of-band Cloudflare config surface No Skipped, with tier guidance
A record that verified but the write failed Yes Per-record failure, with a reason
Why a reserved binding refuses the whole restore

Before any write, every resolved target binding passes through one reserved-binding choke point. The engine’s own bindings (the signer key, the destination credentials, the operational keys and the rest) are reserved, so a restore can never overwrite the engine’s own secrets, whether the reserved name comes from a target override or from a record’s recovered binding. A reserved binding refuses the whole restore before a single byte is written, the symmetric guard to the read side, which refuses to back the same bindings up. This is why a restore can be pointed at a redirect target safely: a confused-deputy attempt to redirect onto an engine binding is refused, not honoured.

When the target binding is not there

A record can also be skipped for a reason that has nothing to do with its type: the binding it would be written through is not present on the engine running the restore. The plan reports that as target binding not present, kept distinct from unsupported sink for sourceType, so a fixable misconfiguration never reads as an inherent limit of the product.

This matters most when you restore back to the original bindings rather than redirecting. The engine resolves the original target from what the archive recorded, and if that binding is not bound on the engine you are restoring through, every record skips for that reason and the plan reports zero planned writes. It writes nothing at all rather than writing to a binding it guessed at, which is the outcome you want: a restore that cannot find the right namespace should not pick a plausible one.

Read that as a fail-safe, not as a promise that an in-place restore is harmless. Where the original binding IS present, an in-place restore writes over live data exactly as it says it will. The zero-write outcome is what happens when the target cannot be resolved, and it is the reason to read the dry run before every apply: a plan reporting zero planned writes against a run you know holds records is telling you the binding is wrong, not that there is nothing to recover.

The way through is to restore through a binding the engine does hold. Redirect the restore to a scratch binding, confirm the content, then move it where it belongs. That keeps recovery possible while the binding question is sorted out separately, and it never puts recovered data over live data by accident.

Where this fits

Last updated .