Skip to content
downpipes docs

Granular and targeted restores: one record, a prefix, a sample, or a redirect

A restore does not have to be all-or-nothing. You can bring back exactly one record, a prefix of records, a capped sample, or send the restored data to a different binding. This page is for the approver and the operator who narrow a restore deliberately and need to know how each choice changes the blast radius, the friction at the confirm step, and the approval the apply requires.

The thread running through every option is that the approval binds to the exact scope you apply. Narrowing or widening the scope changes the plan hash, which re-arms the dual-control approval, so an approval can never be reused for a different scope than the one it was granted for. For the request, approve and apply lifecycle itself, see dual control.

One scope is deliberately absent. There is no point-in-time or as-of-timestamp picker in the console, so this page never offers “restore as of a time” as a narrowing option. Recovery point is expressed as the newest good run, covered in recovery objectives.

The ways to narrow a restore

Each narrowing is a field on the restore request. They compose with the same dry-run-by-default, verify-before-write and dual-control safety as a full restore, and each only ever reduces what gets written.

Field Scopes by Notes
recordName One exact record name The first-class single-record path. An exact match, never a prefix. Supersedes the prefix selectors when set.
include Key prefixes admitted Empty means all. A prefix, so include: ["user:42"] would also admit user:42x.
exclude Key prefixes removed Exclude wins over include.
maxRecords A count cap Caps both the dry-run preview and a partial apply.
target.binding A redirect destination Sends every restored record to one binding instead of its original. High impact.
d1Tables Chosen tables of one D1 database Restores those tables into a fresh database. Supersedes the prefixes; not combinable with recordName.

Single-record restore by exact name

recordName is the first-class way to restore exactly one record. When it is set, the whole restore is scoped to the single record whose source name equals it, and every other record in the run is reported “not the selected record”. The match is exact, never a prefix, which is the point: where a prefix selector is fuzzy at the edges, recordName resolves to precisely the named record or to nothing at all. It can only ever narrow the scope to one record, never widen it, and when it is set the include and exclude prefixes are ignored because the single exact name is the more specific intent.

If the run holds no record with that exact name, the plan is empty by construction and the engine surfaces an honest “record not found in run” rather than a vacuous success or a silent whole-run restore. So a typo in the name fails loudly with nothing written, instead of quietly doing the wrong thing.

In the console you reach a single-record restore two ways. There is a “Restore one record” field on the plan form whose hint reads that an empty value restores the whole run or the prefixes above, and there is a per-row “Restore just this” affordance on each resolved-destination row of a dry-run plan. Choosing “Restore just this” re-enters the same flow scoped to that one record, running a fresh dry-run, so it flows through the identical dry-run, dual-control and apply path with its own distinct plan hash and approval. A plan that is already a single-record plan does not offer the affordance again, because there is nothing left to narrow.

{
  "runId": "01J...",
  "recordName": "user:42",
  "confirm": false
}
Field Type Meaning
runId string The run to restore from.
recordName string The exact source name of the single record to restore.
confirm boolean Absent or false is a dry run that writes nothing; true applies.

Because recordName is folded into the plan hash, a single-record apply carries its own distinct approval and can never reuse a whole-run approval, and changing which record you name re-arms the approval.

Include and exclude prefix selectors

The include and exclude arrays scope a restore by key prefix, with the same semantics as a source selector: an empty include means every record, and exclude wins where the two overlap. They are prefix matches, so they are the right tool for “everything under this namespace” rather than “this one key”. When recordName is set it supersedes them entirely, because the single exact name is the narrower, more specific intent and exclude cannot re-admit anything once the scope is already one name.

{
  "runId": "01J...",
  "include": ["session:", "cart:"],
  "exclude": ["session:debug-"]
}
Field Type Meaning
include string[] Key prefixes to admit. Empty admits everything.
exclude string[] Key prefixes to remove. Exclude wins over include.

The selectors are part of the plan hash, so changing either one re-arms the approval. An apply must match the exact selectors that were approved.

The maxRecords cap

maxRecords is a count cap that applies in two places. In a dry run it bounds the preview, and in an apply it bounds the write: the apply window is the in-scope plan truncated to maxRecords. So a partial apply is genuinely partial, and the same cap governs what you previewed and what lands.

Separately from the cap, a dry run always returns a bounded sample of resolved destinations so a huge run never returns a huge plan. When the caller gives no maxRecords, the sample is capped at 50 rows. So the preview you read is always a manageable size, whether or not you set a cap.

Field Type Meaning
maxRecords integer Caps the dry-run preview and the apply window to this many records.

maxRecords is bound into the plan hash. Changing the cap re-arms the approval, so an approval granted for a 100-record window does not authorise a larger one.

The 200-record in-account ceiling

The in-account restore, the one the console drives, runs the whole in-scope window inside a single engine invocation, so it is bounded to 200 records. A restore whose in-scope window is larger than that is refused before any byte is written, by both the dry run and the apply, with a message that names the limit and points you at the two ways forward: recover the full run offline with the downpipe CLI, which runs on your own machine with no platform limits, or set maxRecords to 200 or fewer for a bounded in-account restore. This is why a whole-run restore of a large run is refused rather than half-applied, and why a big downpipe’s row shows as blocked in a batch restore.

The 200 is a single budget over everything a restore writes, spent in apply order. Data records come first, then any Cloudflare-config surfaces, then any media re-uploads, all counted together against the one ceiling. So a restore with only a handful of data records but many config or media records is bounded by the same 200, and the ceiling is measured on the combined count.

A D1 database is all-or-nothing inside the window. Because a D1 restore replays as a sequence that must travel together and is not resumable, a database that does not fully fit the window is never partially applied. It is dropped from the applied window and steered offline instead, so a re-run can never collide with a half-loaded database.

Continuing a windowed restore

When a cap leaves records beyond the window unrestored, the receipt reports them as an intentional cap rather than a failure. The window is always the first records of the in-scope plan, not a resume from where a previous window stopped, so you bring back more by re-running with a higher maxRecords, not by paging from an offset. KV, R2, Cloudflare-config and Images writes are idempotent, so the records an earlier window already wrote are simply re-applied and the overlap is safe. A Stream video is the exception: each re-upload is assigned a new id, so re-running a window that already restored a video leaves the earlier copy behind as an orphan. If a window included videos, start the next window past them rather than overlapping, or restore the videos once on their own.

A D1 database left unrestored by the window is the one exception to that advice. An in-account D1 restore cannot resume, so the guidance for a D1 left outside the window is to recover the full run offline with the downpipe CLI, never to re-run with a higher cap.

Redirecting a restore to one binding

A redirect sends every restored record to a single target binding instead of its original source binding. This is a high-impact change, and the console treats it as one. Because every sampled record resolves to the redirect target, which is the whole point of a redirect, the dry-run preview shows the redirect destination on every row, and the blast-radius cues reflect it. On the plan itself, the impact banner names the redirect target directly, in a sentence that every record is written to that binding rather than its original source binding, so the impact is explicit before you confirm. A danger-toned “Redirect” chip naming the exact binding appears later, as the value of the Target cue on the pending request in the approver’s approvals inbox, not on this pre-confirm plan.

The confirm friction escalates with the blast radius. A small, same-binding restore uses a single confirm modal, but a redirect always escalates to type-to-confirm regardless of how few records it touches, and so does restoring a run that is not the latest. When a redirect escalates, the value you type to confirm is the target binding name, so the confirmation is bound to the exact destination you are redirecting into.

{
  "runId": "01J...",
  "target": { "binding": "KV_RESTORE_STAGING" },
  "confirm": true
}
Field Type Meaning
target.binding string The single binding every restored record is written to.
target.namespaceId string Overrides the recovered KV namespace name (optional).
target.bucketName string Overrides the recovered R2 bucket name (optional).

The target binding, namespace and bucket names are all part of the plan hash. Redirecting, or changing the redirect, re-arms the approval, so a redirect apply carries an approval granted for that exact redirect and no other.

Which binding names are valid, and what is refused

The target must be a binding your engine actually holds. A redirect target you type is used directly as the binding to write through, so it has to be present on the engine already: a source binding your downpipes attached (whose name you chose when you attached the source), or one following the KV_<namespace>, R2_<bucket> or D1_<name> convention. A name the engine does not hold is not quietly accepted: the plan refuses with a “target binding not present” reason and nothing is written, so a typo in the redirect target fails visibly rather than landing your data somewhere unexpected.

The engine’s own bindings, its signer key, its destination credentials and its operational keys, are reserved. A redirect onto a reserved binding refuses the whole restore before a single byte is written, the mirror of the read side that refuses to back those same bindings up. This is what makes a redirect safe to point anywhere: a confused-deputy attempt to write archived data over an engine secret is refused, not honoured.

A redirect writes every record through the sink for its own source type, and the engine does not check that the target binding is that type, so you must point a redirect only at a binding of the same type as the records you are moving. A type mismatch is not caught and not skipped: the record is written through its own sink to whatever resource the binding names, so it lands in the wrong place, and depending on the direction it then either fails its post-write verification or is still counted as restored. To move a run that mixes source types, redirect one type at a time, each to a matching binding, rather than sending them all to a single binding.

A cancel does not undo what is already written

At the type-to-confirm gate a cancel stops further writes, but it does not roll back records already written; the engine does not roll back. A non-latest restore writes older data over current data, which is why it escalates the confirmation. Review the plan and the blast-radius cues before you apply.

D1 table-subset restore

A D1 database backs up as a per-table sequence, so you can restore a chosen set of its tables rather than the whole database. d1Tables names the database and the tables to bring back. The restore goes into a fresh, empty D1 database: the header re-creates every table so the schema is intact, and only the tables you named are populated with rows. A D1 restore is always into a fresh target, because its rows load over several non-atomic batches and a non-empty target is refused, so you restore the subset into a new database and repoint or copy from there rather than editing a live database in place.

Table names are matched to the backup case-insensitively, and a name that is not in the backup is refused with an honest “table not found”, so a typo fails loudly with nothing written. d1Tables supersedes the include and exclude prefixes for that database, and it cannot be combined with recordName, because the two are different and non-composable intents.

{
  "runId": "01J...",
  "d1Tables": { "database": "app_db", "tables": ["users", "orders"] },
  "confirm": false
}
Field Type Meaning
d1Tables.database string The D1 database to restore a subset of.
d1Tables.tables string[] The table names to restore, matched case-insensitively to the backup.
d1Tables.createOnly boolean Optional. When true, the fresh database is created with only these tables (a minimal extract) instead of the full schema.

d1Tables is folded into the plan hash, so a table-subset apply carries its own distinct approval and changing the database, the tables or createOnly re-arms it.

Create only the selected tables

By default the fresh database keeps the full schema: every table is created, the tables you named hold rows, and the rest are empty. Set createOnly to true for a minimal extract instead, where only the selected tables are created, along with the indexes and triggers defined on them. A selected table’s foreign key to a table you did not select stays as a dangling reference, which SQLite accepts because a restore runs with foreign-key enforcement off. Because createOnly changes what the restore creates, it is bound into the plan hash and carries its own approval, distinct from a full-schema restore of the same tables.

The foreign-key dependency lint

When you select a child table but not the parent it references, the dry-run plan flags it. Each dependencyWarnings entry names the selected child table and the parent that is present in the backup but not in your selection, so you can widen the selection before you confirm. The warning is advisory, not a gate: a D1 restore inserts rows with foreign-key enforcement off, so the child’s rows still load, but its references point at rows that were not restored. In the console this reads as a calm caution on the plan, above the confirm step.

In the console the D1 table-subset controls live in a “D1 table-subset restore” section on the plan form: a database field, a comma-separated tables field, and a “create only these tables” toggle. Filling the database and tables scopes the restore to those tables. The selection re-arms the plan the same way the other scope fields do, and it is bound into the plan hash so the approval matches the apply.

Every narrowing re-arms the approval

The single rule that ties the page together is that the approval binds to the exact scope you apply. The plan hash is computed over the run, the target binding, namespace and bucket names, the selectors, the record cap, the record name and the D1 table-subset selection. Change any one of those decision fields and you get a different hash, which simply has no matching approval, so the apply is refused until a fresh request and a fresh approval are raised for the new scope.

This is why widening or changing scope voids a prior approval. An approval is single-use and bound to one plan, so a granular restore you approved cannot be silently broadened into a whole-run restore at apply time, and a whole-run approval cannot be narrowed and reused either. The console recomputes and shows the plan hash from the request so you can see the exact value an approval binds to, and the engine recomputes and gates on it server-side so the binding is the engine’s, not a value a client could choose.

Media restore (Stream and Images)

Media re-upload is not a narrowing but an additional restore context, alongside the Cloudflare-config context. A Stream video or an Images file is captured as real bytes in the archive, and a restore can re-upload it back into your live account rather than leaving it for out-of-band handling. It is opt-in and safe by default: nothing is uploaded unless you supply an edit-scoped Cloudflare token with access to Stream and Images together with the account id, and confirm the apply. A dry run reports which files would upload and writes nothing. The re-upload only ever creates, it never deletes, so it cannot brick a live asset.

In the console the controls are a “Media restore (optional)” section on the plan form, a media edit token and the account id. The account id is the 32-character hexadecimal identifier the media was backed up from, shown as the Account ID in the right-hand column of your Cloudflare dashboard’s account overview. Stream and Images are account-scoped, so there is no zone id here. Both parts are required together: supply only one and the plan is built without the media context rather than half-applying it.

The identity a file keeps on re-upload depends on the service.

Record On re-upload Identity
Images file Re-uploaded to its original id when that id is free. If a different image already holds the id, the re-upload is refused for that record rather than overwriting it; a byte-identical image is treated as an idempotent re-restore. Keeps its original id
Stream video Re-uploaded through Stream’s direct upload, which transcodes every upload, so the service assigns a fresh uid. The receipt reports an id map from the old uid to the new one so you can update references. Gets a new uid
Metadata or inventory record Informational; re-create from the verified snapshot. Not a file upload

One size bound and one precondition are worth knowing before you rely on it. An in-account re-upload is held whole in memory, so a media file larger than 25 MiB, image or video alike, is surfaced out of band for manual re-upload from the verified bytes rather than failing the restore. And only a record that captured the file’s actual bytes can be re-uploaded: a record whose stored value is a capture marker, meaning the file was not captured as bytes, is reported as captured but not re-uploaded. Each re-upload is then proven, an image by reading the live file back and re-hashing it against the archived bytes, a transcoded video by confirming the new uid resolves.

Where this fits

Last updated .