Data residency and what leaves your account
Residency in downpipes is a decision you make when you choose where backups land, not a concession the vendor grants you. Your backups are written to a destination you select, the engine that writes them runs inside your own Cloudflare account, and the vendor holds no credential for either. This page sets out where your data and the engine’s metadata sit. The page is for an auditor or a risk reviewer who needs to state where the data resides.
The short version is that the encrypted archives live in your destination, and the engine and its operating metadata live in your Cloudflare account. The console contacts the vendor only to redeem a licence claim code. That request carries the code and the name of the zone that serves your console, plus your Cloudflare Account ID when the engine knows it. The engine reports residency from the destination kind. The console flags the S3-compatible, Google Cloud Storage and Azure Blob kinds, which can put your data outside your Cloudflare account; an R2 bucket in a second Cloudflare account still reads as in-account R2 (see the note on the relabel below). The rest of this page is the detail behind each of those claims.
Where the bytes are: ciphertext, metadata, and the vendor
It helps to separate three things, because they sit in different places and an audit needs them named apart.
| Thing | Where it lives | Who can reach it |
|---|---|---|
| Backup ciphertext (the sealed archives) | The destination you choose: an in-account R2 bucket, an S3-compatible store, or an Azure Blob container | You, and anyone you grant access to that store |
| Engine operating metadata (run history, the audit log, presence-only status) | A Durable Object in your own Cloudflare account, served on your own custom domain | You, through the console served on the same origin |
| Vendor-held data | No backup data and no engine metadata. A licence claim can add your Cloudflare Account ID and console zone to your licence record. | The vendor holds no Cloudflare token, no destination credential, and no standing seat |
The engine runs as a Worker in your account, and the console is served on the same address: every admin call stays on that one origin, and the console contacts downpipes only when you redeem a licence claim code (renderConnection, console/src/screens/settings/sections.ts). So the metadata that describes your estate, the run history and the tamper-evident audit log, is account-resident by construction. The vendor is not a place your data passes through; it is a software supplier that holds no key to your account.
The archives are sealed before they leave the engine, so what sits in the destination is ciphertext, wrapped to your recovery recipients. Choosing an S3-compatible store moves where that ciphertext rests, not whether it is encrypted. Residency is about where the bytes sit and who can reach the store, which is a separate question from confidentiality.
You choose the destination; the engine reports it
The engine does not pick your destination and cannot move it. You set it, and the engine reports back the kind it resolved so the console can state residency rather than assume it. The status route’s destination facts are four coarse fields and no value: a destKind enum, a destConfigured boolean, a destResolved string that names the unresolved case rather than leaving it null, and a destAmbiguous boolean that carries a cause. A fifth, statusSource, says whether those facts were read live or fell back to the environment mirror. None of them echoes the endpoint, the bucket name, the region, or any access key (buildStatus, engine/src/admin/status.ts). That is a deliberate redaction, so the residency read itself cannot leak where you write.
The engine resolves five destination kinds from that host: in-account R2, an S3-compatible store, Google Cloud Storage, Azure Blob Storage, and no kind resolved at all (DestProvider and providerForEndpoint, engine/src/dest/provider.ts). The Data and residency panel shows its own line for each of them (residencyBody, console/src/screens/settings/sections.ts).
In-account R2. The console reads this as “In-account R2 (cannot enforce Object Lock).” This is the no-custody destination: an R2 bucket in your own account. A deploy-time DEST_R2 binding reaches it with no destination access key to configure, hold, or expose. A destination you set from the console is different: it reaches the bucket through its S3-compatible endpoint with an R2 access key, and it still reports as in-account R2 (see the relabel below). The bytes stay in Cloudflare R2.
An S3-compatible store. The console reads this as “S3 (verify the region and account; this can move data out of your account).” Read this line carefully in an audit. An S3-compatible destination can put your ciphertext in a store and a region you should verify, and can move data out of your Cloudflare account. The engine does not decide that is wrong, because a deliberate copy to an Australian S3 region may be exactly what your obligations require; it states that the bytes are going somewhere you need to confirm.
Google Cloud Storage. The console reads this as “Google Cloud Storage (verify the project and bucket; this moves data out of your Cloudflare account).” Google Cloud Storage is reached through its S3-interoperable API, so on the wire it is an S3-compatible store, but it is named rather than folded into the S3 line: an operator reading this panel is asking where their archives are, and “S3” would name the wrong company. The residency caveat is the S3 one and slightly firmer, because the destination is definitely outside your Cloudflare account rather than possibly outside it.
Azure Blob Storage. The engine resolves this kind from an <account>.blob.core.windows.net endpoint and reports destKind: "azure", and the residency reading is the Google Cloud one: your ciphertext rests in an Azure storage account, definitely outside your Cloudflare account, so the storage account and the container are yours to verify and to name in an audit. Azure is not reached over the S3 API at all. It has its own client (engine/src/dest/azure-blob.ts). That client signs with the account key (Shared Key), or uses a SAS token or a Microsoft Entra service principal. The client and its credential change how the bytes are authenticated on the way and change nothing about where they land.
No destination kind set. The panel reads “No destination kind set.” Read this one carefully, because two different situations reach it. The ordinary one is first run: you have not chosen, and the engine reports not-ready rather than guessing a destination. The other is an engine configured with an R2 binding and S3 credentials at once and no DEST_KIND to choose between them. The destination factory refuses that ambiguity rather than picking one, because a silent choice would split the archive across two stores, and the refusal is coarsened into the same not-ready answer (resolveDestKind, engine/src/admin/status.ts).
An auditor reading the panel alone cannot tell those two apart, and the difference matters: the first means nothing is configured, the second means two things are and the engine will not guess. The engine does distinguish them. destAmbiguous on the status report carries the cause, and the remedy is one line, setting DEST_KIND to the store you meant. The console does not render that field, so the distinction shows in the support bundle’s status report and not on the Data and residency panel.
R2 reached through its S3 endpoint
There is one subtlety the engine handles so it does not mislabel your own bucket as foreign. A destination you set from the console is stored in an S3-shaped record, because the console cannot create a Cloudflare binding at runtime. But an R2 bucket is also reachable through its own S3-compatible endpoint, <account>.r2.cloudflarestorage.com, where <account> is the Cloudflare Account ID of the account the bucket lives in, the 32-character identifier the Cloudflare dashboard shows in its R2 area. That Account ID is what forms the endpoint host, and the console asks you for it in the two cases where the endpoint has to come from an id you type: when discovery could not name the engine’s own account, and when you tick This bucket is in a different Cloudflare account to point the destination at an account other than the one discovery named. In the ordinary case neither applies, the endpoint is derived from the discovered engine account, and there is nothing to type. If the engine blindly read the S3 shape, it would call your own in-account R2 bucket a third-party S3 store, which is wrong and would understate your residency.
So the engine inspects the endpoint host. When it recognises the Cloudflare R2 endpoint, it relabels the destination as in-account R2 rather than foreign S3, because the bytes rest in Cloudflare R2 regardless of which API shape reached them (providerForEndpoint, engine/src/dest/provider.ts, and the console-set branch of buildStatus, engine/src/admin/status.ts). The relabel is a residency correction, not a credential claim: it says where the bytes rest, which is the question residency asks.
The same derivation names Google Cloud Storage, whose S3-interop endpoint is a single documented host, and answers s3 for every store it does not recognise, so an unrecognised S3-compatible destination is labelled S3.
It also names both Azure Storage endpoint families, and the two resolve to opposite outcomes. An <account>.blob.core.windows.net endpoint resolves to azure and is a destination: it does not speak the S3 API, so the engine dispatches it to its own Azure client rather than to the S3 one. An <account>.dfs.core.windows.net endpoint is Data Lake Storage Gen2, which is a different protocol rather than another address for the Blob one, and it is refused by name with the remedy attached, pointing you at the Blob endpoint for the same account (unusableEndpoint and UNUSABLE_ENDPOINT_REASON, engine/src/dest/provider.ts). Naming it is what lets the refusal say which store it recognised instead of sending you to audit a credential that was never the problem.
The relabel reads the endpoint host, not the account id inside it
providerForEndpoint matches any *.r2.cloudflarestorage.com host and never compares the account in that host against the engine’s own account (engine/src/dest/provider.ts). So a destination you deliberately pointed at a second Cloudflare account, using the different-account tick described in putting an R2 bucket in a different Cloudflare account, still reports as in-account R2. The label is telling you the archive rests in Cloudflare R2 rather than in a third-party S3 store; the Account ID on the destination is what tells you which Cloudflare account holds it. An audit that has to name the account should read that field, not the residency label.
The fields, briefly
A destination is a handful of fields, and each carries a rule worth stating for an audit.
| Field | Applies to | The rule |
|---|---|---|
| Endpoint | S3-compatible and Azure Blob destinations | Must be an https URL. The engine additionally screens the host against private, link-local and metadata addresses and refuses them, an SSRF guard, so the endpoint cannot be pointed at an internal service. A deployed engine has no loopback exception: a plain http://localhost, http://127.0.0.1 or http://[::1] endpoint is refused like any other http endpoint (requireHttpsEndpoint, engine/src/dest/s3-addressing.ts). See the threat model for the full rule. |
| Bucket | R2, S3 or Azure (the container name, there) | Proven writable by a live write-probe at save time rather than checked against a name format, so a name that reads as valid but is not reachable and writable is refused. |
| Region | S3-compatible destinations | Has no effect on an Azure Blob destination, whose requests carry no region. Defaults to auto, which suits R2 and most S3 providers. Amazon S3 itself rejects auto and answers a bucket in another region with an HTTP 301, so set the bucket’s real region there. Under STS AssumeRole it must be the role’s real AWS region, never auto, and the engine refuses auto in that case. |
The support bundle does not leak your destination
Residency is something you configure and verify, and it is not something the support bundle quietly carries out of your account. When you raise a ticket, the diagnostics bundle aggregates presence-only status, which includes the destination kind enum but not the destination itself. Because the bundle embeds the same status report, it inherits the same redaction: the endpoint, the bucket, the region, and any access key are absent by construction (buildStatus embedded in the bundle, engine/src/admin/support.ts).
So the bundle can tell support that you write to R2 or to S3, which is a coarse fact useful for diagnosis, without revealing where. Residency is a fact you state and verify yourself; it is not exfiltrated by the diagnostic path. For exactly what the bundle does and does not contain, see the support bundle.
The bundle and the audit feed carry operator identity, so neither is free of personal data even though neither carries your destination. The bundle includes your own downpipe names and operational facts, and the separate audit feed records, per event, the actor email, the stable actor subject, the auth method, and the source IP. A role appears only inside an event’s target, as the affected member’s role or a group mapping’s role, never as the acting operator’s role. Treat them as evidence about your operators, not as anonymous telemetry, when you reason about what leaves your account.
Residency as a configuration decision, not a vendor concession
The principle worth taking to a risk team is that backup residency is a configuration decision, not a vendor concession. The SOCI and CIRMP mapping the website publishes makes the same point against the material-risk entry about storing sensitive operational information outside Australia. It says you choose the destination, and that its region is a setting of the bucket you create. It also states that downpipes does not verify a destination’s region.
That framing matters because it inverts the usual question. With most backup vendors, residency is something you negotiate and the vendor concedes, because the vendor holds your data. With downpipes the vendor holds none of your backup data, so residency is yours to set and yours to evidence. The engine writes where you point it and reports that destination; the choice, and the proof of the choice, stay with you.
For deeper reasoning about what the vendor and engine can and cannot hold, including the optional operational key posture, see the no-custody trust model. For how that posture interacts with support specifically, see getting support without giving us access.
Where this fits
Residency is where your estate lands; the rest of the picture is how it gets there and how you read it back.
Setting and verifying the destination
The destination is set from the console, not from the engine’s environment by hand, and the write is verified before a run pins it. The mechanics of choosing a destination, the write-probe that confirms it, and what an in-account R2 versus an out-of-account S3 choice means for credentials live with the destination surface. The Data and residency panel under Settings is the at-a-glance read of the kind the engine resolved.
Reading residency on the map
The topology map reflects residency visually: an in-account R2 archive node reads differently from an out-of-account S3 archive, and the destination note states the model in words rather than leaving it implied. See the topology map for how the map labels the destination from the same destKind this page describes.
To see residency rendered as part of the live estate, read the topology map. To understand the support path that deliberately does not carry your destination, read the support bundle. When you are leaving the product, leave downpipes explains why retiring the engine does not move your data: your backups already live in the destination you chose.
Last updated .