Secrets and Workers: high-assurance capture with deliberate restore limits
Secrets and Workers are the two sources whose restore is, on purpose, not a destructive write back into your account. A secret’s value is captured and sealed encrypted into the archive, but its restore is out of band only: the engine never writes a secret value back into your account. A Worker is captured as code, settings, a version inventory and its cron triggers, with every secret binding redacted to a name-and-type checklist, and its restore is reprovision-only: the engine never blind-redeploys a script. This page explains both, and what each one proves. It is written for an evaluator weighing what these two sources do and do not promise.
The reason to treat them together is that they share a discipline. Both capture wiring and content that are high-consequence to recover incorrectly, so both prove the snapshot is recoverable while refusing to perform the write that could do harm. A captured snapshot is verified recoverable, but the act of putting a secret back, or redeploying a Worker, stays in your hands.
Secrets are an explicit-bind source
The engine only ever reads the values of the secrets it was explicitly granted. A secret is backed up only if the Worker is bound to it; there is no read-all over the store’s values. The value is read at runtime through that binding and lives in isolate memory only until it is sealed; the plaintext is never logged, never written to a descriptor field, and never written to Durable Object state (SecretsSource.crawl, engine/src/sources/secrets.ts). What lands in the archive is the wiring and the value itself, the value sealed under the run’s encryption so the secret is recoverable. What never lands anywhere is the plaintext in the clear.
| Captured | Persisted to the backup? |
|---|---|
| The secret’s value | Yes, sealed encrypted into the archive so the secret is recoverable. The plaintext is read at runtime, held in isolate memory only until it is sealed, and is never logged, never written to a descriptor field, and never written to Durable Object state. |
| The store and the engine binding | Yes, as wiring, so a restore can identify which secret this was. |
| The secret’s scopes and its comment | From engine 0.3.6, yes, as wiring. The engine reads them from the Secrets Store. |
| Every Worker that binds the secret, with the binding name that Worker uses | From engine 0.3.6, yes, as wiring. The record also says if this list is complete or unknown. |
Each wiring field is attached to the record only when it is set, so a record carries the minimal descriptor and nothing more (RestoreDescriptor, engine/src/sources/types.ts).
From engine 0.3.6, the engine reads the scopes, the comment and the Worker wiring once per run, with the read-only discovery token (readSecretsWiring, engine/src/sources/secrets-wiring.ts). It finds the Workers in the settings of each Worker script: every Secrets Store binding that names this store and this secret. The Secrets Store list returns the names, scopes and comments of every secret in the store. The settings of a script return all its bindings, plain-text values included. The engine keeps only the entries that match your configured secrets, and none of these reads returns a Secrets Store value. It reads only the accounts in your discovery scope.
The token is optional for this source. If there is no token, or the token cannot read the Secrets Store or the settings of one Worker, the run still seals every value. The record then marks the Worker wiring as unknown and carries no list, and the run counts the cause. The same applies when the Worker settings reads do not fit in the run’s time and request budget. That can happen in an account with many Worker scripts. The metadata read stops before it can fail the run.
A record never carries a partial list of Workers, because a short list would tell you that no other Worker uses the secret. The record marks the list complete only when the secret’s name is in the store and the list holds the engine’s own binding. The estimate path never reads a secret value to size the backup: it counts the in-scope secrets and reports an unknown byte size, so even sizing the backup does not touch a value.
Secret values are captured encrypted, and restored out of band
A downpipes backup captures a secret’s wiring and its value. The wiring is the store and the engine binding, and from engine 0.3.6 also the scopes, the comment and the Workers that bind the secret. The value is sealed under the run’s hybrid post-quantum encryption while it is still in the isolate, so the archive holds the encrypted value and never the plaintext: nothing is logged, and no descriptor field, log line or Durable Object state holds it. The ciphertext lives only in your own destination, under keys only you hold. Putting a secret back is out of band, because Secrets Store bindings are read-only at runtime, so the engine never writes a secret into your account.
Why secrets restore out of band only
Cloudflare Secrets Store bindings expose a read at runtime but no runtime write back, so there is no in-account path the engine could use to put a secret value back. The engine treats this as an inherent platform limitation, not a transient error, and reports it at every step.
A secrets record is never a planned write. The restore planner routes every secrets record to an out-of-band list while it builds the plan, so a secrets record never appears in the dry-run planned-writes count and the apply phase never attempts a write that would always fail (buildRestorePlan, engine/src/admin/restore-plan.ts). In both a dry run and an apply, the record is surfaced with guidance that names the cause and the remedy (SECRETS_OUT_OF_BAND_REASON, engine/src/admin/restore-sinks.ts):
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
The offline reader is named because it is the only path that puts a recovered secret value in front of a person: the console never displays one. See break-glass recovery.
This does not mark the run failed. An out-of-band secret is a known platform limitation rather than a runtime fault, so it does not set the result to not-ok and does not contribute to the failure list (runApply, engine/src/admin/restore-apply.ts). A restore whose only unwritten records are secrets is a success with an out-of-band note, not a failure.
The safety that matters here is that plan-time routing, and not a guard at the write. The secrets write target does refuse the write, throwing a clear message that the secret has no runtime write path and should be restored out of band (SecretsRestoreSink.put, engine/src/dest/restore-sink.ts), but a restore never reaches it: the record leaves the plan before a write target for it is constructed. Treat it as the second line, not the reason you are never told a secret was restored when it was not.
The Workers source captures four record kinds per script
The Workers source snapshots the account’s deployed scripts through the Cloudflare REST API with the engine’s read-only discovery token; like the configuration source, and unlike every binding source, it is account-scoped and needs no Workers binding. It exists because a customer’s Worker code and wiring are otherwise unrecoverable, including the downpipes engine and console Workers themselves. Each script produces up to four records, name-prefixed by the script id so the selector can scope by name.
| Record kind | Name | What it holds |
|---|---|---|
| Content | <id> | The recoverable code: the raw module or bundle bytes, stored as the record value with no re-encoding. |
| Settings | <id>/settings | A canonical-JSON settings record: bindings, the compatibility date and flags, observability, limits, placement and tags, with secret bindings reduced to a checklist. |
| Versions | <id>/versions | A small inventory of version id, number and created-on time, best-effort. |
| Schedules | <id>/schedules | The cron triggers: each cron expression with its created-on and modified-on times. |
The versions record is an inventory only. It deliberately keeps the version id, the number and the created-on time, and drops each version’s own bindings and metadata, because per-version detail can re-contain secret references (WorkersSource.crawl, engine/src/sources/workers.ts).
Worker secret bindings come back redacted
A Worker’s secret bindings have a write-only value the Cloudflare API never returns, so the settings record reduces every one of them to a name-and-type entry and carries no value. The redaction covers a per-Worker inline secret, a secret key, and a Secrets Store reference (SECRET_BINDING_TYPES, engine/src/sources/workers.ts). For each, the record keeps only the binding’s name and type in a reprovision checklist and drops everything else, including any store id, so the record can never become a value oracle. A non-secret binding (a KV namespace, an R2 bucket, a D1 database, a service or a queue) is kept verbatim, since it is account metadata and not a secret (redactSettings, engine/src/sources/workers.ts).
No Worker secret values are captured
The backup holds no Worker secret values. A secret binding is reduced to a name-and-type entry in a reprovision checklist, with everything else dropped, including the store id. The checklist tells an operator which secrets to re-create at re-deploy; it never holds what they were.
Workers restore is reprovision-only
The engine never blind-redeploys a customer’s Worker from a backup, because redeploying the wrong code or wiring could brick a live service. Instead the snapshot is verified recoverable and surfaced out of band with re-deploy guidance, so the operator re-deploys deliberately (buildRestorePlan, engine/src/admin/restore-plan.ts). There is no destructive Workers restore path: a Workers record is never resolved to a write sink.
The guidance is specific to the record kind. It is a pure function of the record name, so the wording is the same wherever it appears (workersRestoreGuidance, engine/src/admin/restore-sinks.ts).
| Record | Guidance |
|---|---|
<id> (content) | Re-deploy the script code from the verified content snapshot, through wrangler deploy or the Workers API, then apply its settings record. |
<id>/settings | Re-create the bindings and secrets from the verified settings snapshot. The secret values were never captured, so the checklist lists their names and types to re-provision. |
<id>/versions | A version inventory only, informational; re-deploy from the script content record. |
<id>/schedules | Re-create the cron triggers from the verified schedules snapshot, so the re-deployed Worker keeps its schedule. |
The recoverability the guidance relies on is proven, not asserted. The blind restore test decrypts and hash-checks every Workers record, including the code, the settings and the versions inventory, writing nothing and surfacing no plaintext, so the operator knows the snapshot is intact before they re-deploy from it.
Fail-open per script, loud on a total failure
The Workers crawl tolerates a gap in one script or one aspect without failing the whole snapshot, but it refuses to report a successful snapshot that captured nothing. A script or a sub-call the token cannot read, a missing scope, a deprecated endpoint, a transient fault that survived retry, or an oversized bundle, becomes an unavailable marker record rather than a hard failure, so one bad script never breaks the run (WorkersSource.crawl, engine/src/sources/workers.ts).
Two conditions still throw loudly, because each means the snapshot is empty for a reason the operator must know. If the list call itself fails, that is a broken or under-scoped token rather than a per-script gap, so the crawl throws and names the likely missing Workers Scripts read scope. If a crawl from the start attempted at least one aspect and every one failed, that is also a broken token or a lost scope. The crawl then throws rather than reporting a snapshot that captured nothing.
The engine marks a script body over the size limit unavailable and does not archive it. From engine 0.3.6 the engine refuses such a body before it holds all of the body in memory. If the declared length is over the limit, the engine reads none of the body. If the response declares no length, the engine stops as soon as the bytes pass the limit. It then holds no more than the limit and the chunk it is reading.
Before engine 0.3.6 the engine read the whole body first. The cap is WORKERS_SCRIPT_SIZE_LIMIT, 128 MiB, far above any realistic Worker bundle, since the platform upload limit is a few mebibytes compressed (engine/src/sources/workers.ts).
Worker bodies are the buffered exception
Most large captures in downpipes are streamed, but a Worker script body is the deliberate exception: it is buffered, capped at 128 MiB, not streamed. The engine buffers a script bundle past the cap too, then marks it unavailable rather than archiving it (engine/src/sources/workers.ts).
| Capture | Held whole in memory? |
|---|---|
| A large R2 object | No, streamed in windows. |
| The D1 schema-plus-rows dump | No, produced and sealed incrementally. |
| A Worker script body | Yes, buffered, capped at 128 MiB. |
| A secret value | Held in isolate memory for the single sealing pass, then dropped; the plaintext is never persisted, the encrypted value is. |
Secrets seals whole in one slice and is non-resumable: it does not implement the checkpointed crawl that the large binding sources use, so it is captured in a single pass rather than resumed across invocations. Workers is different: it is resumable, via a checkpointed per-script crawlFrom whose resume token is the last fully-yielded script id, which lets a large account span the backup across multiple invocations (WorkersSource, engine/src/sources/workers.ts). By default one invocation spends up to 700 subrequests, and each script costs up to four API reads (DEFAULT_SLICE_SUBREQUESTS, engine/src/seal/budget.ts).
What a snapshot proves
A captured snapshot is proven recoverable; it is not verified end to end in your browser. Two recoverability proofs back a snapshot, and both are out of band of the console.
| Proof | What it proves |
|---|---|
| Blind restore test | Decrypts every in-scope record to a discard sink, checks each plaintext hash, and writes nothing and surfaces no plaintext. It proves the records, including a Worker’s code, settings and versions, decrypt and match their signed hashes. |
| Keyless attestation | Verifies the manifest signature, structural completeness (shard hashes and count) and the RUNLOG anti-rollback, with no decryption key and no plaintext read. |
Read the attestation for what it is: an engine-side completeness and anti-rollback check, not a content read. It runs even in the break-glass-only posture where there is no in-account read-back key. Full verification is out of band, because the verifying key is not in the browser; the console asserts a signature is present and well-formed rather than performing the cryptographic verification itself. The recoverability of a Worker snapshot or a secret’s value is therefore something you prove on demand, then act on deliberately.
Where this fits
These pages set the two sources in their wider context.
No-custody trust model
Why a secret’s plaintext never leaves your isolate and the vendor holds nothing, the trust property behind these restore limits.
What restore can and cannot write back
The full account of which sources write back in account and which are out of band or reprovision-only.
Sources overview
What each of the eight source types captures and how each restores.
Prove recoverability
The blind restore test and the keyless attestation, and why verification is out of band.
Last updated .