Multi-region
Regions, zones, residency, routing, and why sessions do not fail over across regions.
Regions and zones
A region is a geographic location with its own control-plane cell, data plane, storage and egress. A zone is an independent failure domain inside a region — separate power, network and racks.
| Region | Location | Zones | Tiers | Status |
|---|---|---|---|---|
bom1 | Mumbai, India | bom1-a, bom1-b, bom1-c | 0–4 | Primary |
iad1 | N. Virginia, USA | iad1-a, iad1-b, iad1-c | 0–4 | Primary |
fra1 | Frankfurt, Germany | fra1-a, fra1-b | 0–4 | Primary |
sfo1 | San Francisco, USA | sfo1-a, sfo1-b | 0–3 | Secondary — no tier 4 |
sin1 | Singapore | sin1-a, sin1-b | 0–3 | Secondary — no tier 4 |
Tier 4 needs desktop-class hardware at 1:1 oversubscription, so it is only in regions where that
capacity is justified. A tier 4 create in a region without it fails with tier_unavailable_in_region
rather than silently placing elsewhere, because silent placement would violate residency.
What is global and what is regional
| Global | Regional |
|---|---|
| Organizations, projects, members, roles | Sessions, runs |
| API keys | Volumes, snapshots, recordings |
| Templates (definition) | Template layer caches and pre-booted snapshots |
| Policies (definition) | Compiled policy artifacts |
| Price lists, invoices, credits | Usage events before aggregation |
| Audit log (replicated) | Node inventory, placements, pools |
Global state lives in a single-primary Postgres replicated to every cell. Writes are rare and latency-tolerant; reads are served locally.
Routing
api.workspaces.build anycast -> nearest healthy control cell
a call about an existing session is forwarded
to that session's region, transparently
connect.workspaces.build resolves to the session's region directly;
streams never cross regions
*.run.workspaces.build resolves to the session's region's edge-router
A control call about a session in another region is forwarded, not rejected. The caller sees one API and does not have to know where its session lives. Data calls are never forwarded — a stream that crossed regions would add tens of milliseconds to every frame, so the gateway hostname resolves regionally from the start.
Choosing a region
In precedence order:
- Residency policy. If the organization or project is pinned, that is the only answer. A request for another region is refused.
- Explicit
regionon the request. - Volume or snapshot locality. Restoring a snapshot places in that snapshot's region.
- Project default region.
- Lowest-latency region from the caller, measured at the edge.
Residency
{
"residency": {
"mode": "pinned",
"regions": ["fra1"],
"applies_to": ["sessions", "volumes", "snapshots", "recordings", "logs"],
"allow_cross_region_copy": false
}
}
Enforcement is at four layers, because one of them being misconfigured must not be enough:
admissionrefuses a create in a disallowed region.placerfilters disallowed regions out of candidates before scoring.- The storage layer refuses to materialise a volume, snapshot or recording outside the allowed set.
billingand the usage API keep aggregates regional when the policy says so.
Metadata — the fact that a session exists, its ID, its state, its usage totals — is global, because billing and the API are global. Content never is. This distinction is in the data processing agreement explicitly, because it is the question every regulated customer asks.
Failure behaviour
| Failure | Blast radius | Behaviour |
|---|---|---|
| One zone | That zone's sessions | Sessions fail with host_failure; new placements go to other zones; pools rebalance within the region |
| One region's data plane | That region | Sessions fail; control plane keeps serving reads; creates in that region refused |
| One control-plane cell | New creates in its region group | Running sessions unaffected; the dashboard and API degrade to read-only for those regions |
| Global Postgres primary | Writes to global state | Automatic failover, typically under 30 s; sessions and execution unaffected throughout |
A session is a live machine with live memory and disk. Transparent cross-region migration would mean moving gigabytes of state across a WAN while the agent is mid-action — slow, and a residency violation waiting to happen. Instead the session fails, the failure is explicit in the API and in a webhook, and the caller decides whether to recreate it. From a snapshot, that recreation is seconds; without one, the work is lost. Snapshot anything you cannot afford to lose.
Building a multi-region integration
const REGIONS = ['bom1', 'iad1', 'fra1']
async function createResilient(spec: SessionSpec) {
const ordered = [spec.region ?? defaultRegionFor(spec), ...REGIONS].filter(unique)
for (const region of ordered) {
try {
return await wb.sessions.create({ ...spec, region })
} catch (e) {
// retry the next region only for capacity, never for quota or policy
if (e.code !== 'capacity_unavailable') throw e
}
}
throw new Error('no region had capacity')
}
Retry only on capacity_unavailable. Retrying a quota_exceeded across regions does nothing —
quotas are per organization, not per region — and retrying a residency refusal would be an attempt
to defeat a control the customer asked for.
Latency between regions
Round-trip times used when planning cross-region work:
bom1 | iad1 | fra1 | sfo1 | sin1 | |
|---|---|---|---|---|---|
bom1 | — | 210 ms | 115 ms | 240 ms | 55 ms |
iad1 | 210 ms | — | 90 ms | 65 ms | 230 ms |
fra1 | 115 ms | 90 ms | — | 145 ms | 160 ms |
If an agent loop is chatty against a session, the client should run in the session's region. Cross-region exec adds a full round trip to every call, which an agent making hundreds of calls will feel far more than the session's start latency.