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, 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.
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 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. Where a stored field cannot be reproduced, the restore drops that field rather than the record, says so in the dry-run plan where it can be known in advance, and counts it on the receipt. A KV expiration that has already passed is the case you are most likely to meet, and it has its own section below.
The restore replaces a KV key or R2 object that already exists in the target under the same name with the archived value. From engine 0.3.6 and console 0.2.7, the dry-run plan lists which planned names exist, and you can choose to keep them instead. See live keys and objects an apply replaces.
| Data class | In-band restore | Fidelity and notes |
|---|---|---|
| Workers KV | Yes | Value plus metadata and expiration; an expiration that has already passed is dropped, see below; 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 |
A KV expiration that has already passed is dropped, and the key still restores
A KV expiration is captured and written back as an absolute instant, not as a remaining lifetime. So a backup that is older than a namespace’s time-to-live carries expirations that have already gone by. A live KV binding refuses a write whose expiration is not at least sixty seconds in the future. This is the ordinary case when you restore an old backup of a namespace that expires its keys.
The restore keeps the key. It writes the recovered value and the key’s metadata and leaves the lapsed expiration off, so the key lands and then does not expire until you set a new time-to-live on it. It does not invent a replacement expiration, because a time-to-live you did not choose is not your data.
You are told twice, and the first time is before you commit to anything:
- The dry-run plan carries the count, and the restore screen shows it as a caution headed “Some records restore at reduced fidelity”. Read it before you approve, because for a namespace of session or cache keys you may prefer to narrow the restore rather than resurrect keys that were meant to have gone. The keys are still counted in the planned writes, because they do get written. The plan states two counts rather than one, and the second is the actionable one: how many expirations had already gone by when the plan was computed, and how many more go by before the last instant an apply of this plan could still be writing, which is twenty-four hours and thirty minutes after the dry run. Keys in that second group are only lost if you take your time, so applying promptly keeps them.
- The receipt records the count under the fields the restore could not reproduce, so the restore reports as applied with reduced fidelity rather than as clean. It is inside the signed and hashed part of the receipt, so it is on the file you download and not only on the screen, and a receipt with the count stripped out no longer verifies. That is your evidence when you re-apply the namespace’s time-to-live afterwards.
A KV key with no expiration is unaffected and restores exactly as it was captured. So does one whose expiration is far enough in the future to survive the apply, which is what the plan’s second count is measuring.
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, so a fault part way through can leave the database partially loaded. That is why the contract is to restore D1 into a fresh, empty database, and why a D1 write failure carries a reason that says so: 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 holds rows in the backup but 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.
What stops you being told a secret was restored when it was not is the restore planner, not a guard at the write. Every secrets record is routed out of band while the plan is being built, before a write target for it is constructed at all, so it never enters the planned-writes count and the apply never reaches a write it would have to refuse. A secrets write target does exist and does refuse the write, but it is not on this path and it does not fire here. See why secrets restore out of band only for the mechanism.
What a backup holds for a secret is its encrypted value together with its wiring. On a restore, a secrets record is surfaced with this guidance:
Secrets Store value, restore out of band: the binding is read-only at runtime, so there is no in-account write path; recover the value from this archive with the offline reader and your break-glass key, then re-create the secret through the Cloudflare API or wrangler
Getting the value into your hands is the offline channel: restore that record with the open-source reader and your break-glass key, to the file sink or the env sink, which is the only path that puts a recovered secret value in front of a person. The console never displays one. See break-glass recovery for the channel and the CLI command reference for the sinks. You then create the secret again yourself, with the value you recovered. Restoring a secret value is something you do deliberately, 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. Restore reflects how few of them can be written back safely. 85 of the 313 carry a write path at all. Of those, 60 form the default set that re-applies when you supply a Cloudflare token and name no scope. The other 25 have a write path that is off by default, because its matching of existing items by Cloudflare’s natural keys is not yet confirmed: a default restore never runs them, and reaching one takes naming it in the request’s surfaces allow-list, which binds it into the plan hash so an approver sees which of those surfaces they are authorising. The other 228 have no write path and are backup-and-preview only.
The surfaces that do re-apply do so 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 ordered surfaces (such as Access dependency order) and the reprovision ones (such as certificate private keys) stay out of band with tier-specific guidance.
A config restore writes to the account you name
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. 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. 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 receipt. A failure is a record that verified but could not be written. 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. If that binding is not bound on the engine you are restoring through, every record skips 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
- The restore flow walks the apply that performs these in-band writes behind the integrity verify.
- Dual control is the optional approval that, when you require it, gates every apply, in band or with out-of-band items.
- Cloudflare config backup and restore lists the exact edit-token permissions a config restore’s apply needs, per in-band surface.
- Proving recoverability covers how a Workers or D1 record is proven recoverable without being written.
- The restore and recovery API documents the underlying restore request and result shapes.
Last updated .