Multi-Region Systems

EU and US Are Partitioned. Can Both Keep Accepting Writes?

The link between two regions fails. Both are healthy, both are serving users, neither can reach the other, and neither can tell whether the other is dead or merely unreachable. Whether both may keep accepting writes has an answer — but it is a property of the invariant, not of your preference, and it is different for different data in the same system.

▶ Run the lab

The question this answers

The question

The regions cannot see each other and both are up. Which of them is allowed to say yes?

The guarantee — the property claimed, and its scope

Per invariant, one of exactly three: (a) the invariant holds throughout, and writes touching it are unavailable in at least one region; (b) the invariant holds throughout because it was pre-factored into per-region shares, and both regions stay available for their own share; (c) both regions stay available and the invariant is not enforced during the partition, with violations detected and repaired afterwards. There is no fourth option, and choosing not to decide selects (c) by default.

Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.

What a node knows — observation versus inference

Each region knows that messages to the other stopped arriving at a particular time. That is all. It does not know whether the other region crashed, whether the link failed, whether the other region is still serving users, or whether the other region believes the same thing about it. Crucially, it does not know whether it is on the majority side of the partition or the minority side — and the two look identical from the inside, which is why "keep serving if you are the bigger half" is not implementable without an external reference.

A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
partitionsplit-brainavailabilityinvariantscap

The scenario, precisely

At 14:02 the transatlantic link degrades and then stops. Frankfurt continues serving European users at normal latency. Virginia continues serving American users at normal latency. Both regions’ dashboards are green for everything local. Both show the other region as unreachable. Both are correct.

Note what the operator sees: not an outage. Traffic is flowing, error rates are normal in each region, and the only anomalies are a replication lag climbing without bound and a peer health check failing. In a great many organisations this state persists for tens of minutes before anyone treats it as an incident, because none of the alerts that fire are the ones people watch.

The decision that must be made in those minutes is not "how do we restore the link" — that is somebody else’s job and it will take as long as it takes. It is: for each class of data, do we keep accepting writes on both sides? And that question has a different answer for the audit log, the user profile table, the username registry and the ticket inventory, in the same system, at the same moment.

Both sides healthy, both sides isolated, both sides wrong about the othersimplified
fra ↔ iad: partitioned — no traffic crossesceu ↔ fra: okcus ↔ iad: okFrankfurt · leader · up — serving EU users, replication queue growingFrankfurt★ leaderVirginia · leader · up — serving US users, replication queue growingVirginia★ leaderUser in Berlin · client · upUser in Berlin▷ clientUser in Chicago · client · upUser in Chicago▷ clientpartitioned
partitionedok
  • Frankfurt — serving EU users, replication queue growing
  • Virginia — serving US users, replication queue growing
What each node believes
  • frabelieves “Virginia is down”✕ and it is false
  • iadbelieves “Frankfurt is down”✕ and it is false
  • frabelieves “I am the surviving region and should keep writing”✕ and it is false
  • iadbelieves “I am the surviving region and should keep writing”✕ and it is false
  • ceubelieves “the system is working normally”✓ and it is true
  • cusbelieves “the system is working normally”✓ and it is true

Every node above is acting on what it believes. Nothing in the cluster tells the mistaken one that it is mistaken.

Work the answer per invariant

Take five invariants from an ordinary system and ask the question of each. The answers differ, and the differences are the whole lesson.

The pattern that emerges: an invariant that is local to one record survives both regions writing, provided each record has one owner. An invariant that is a global aggregate does not, unless it can be split into per-region shares in advance. An invariant that is existence over an unbounded namespace — uniqueness — cannot be split at all, and always requires either a single owner or after-the-fact repair.

InvariantBoth keep writing?WhyWhat it costs
Append-only audit log per userprotocolYesEntries commute; there is no shared state to disagree about. The merged log is the union, ordered by causality where it exists.Ordering between regions is undefined until merge, so any report that assumes a total order is wrong during the window.
A user’s profile record, one owner per userprotocolYesEach user is owned by one region, so a partition splits the *users*, not the records. No two regions write the same record.Users whose home region is on the far side cannot write. Their region is up; their data is not reachable.
Usernames are globally uniqueprotocolNoUniqueness is a claim about absence, and absence is exactly what an isolated region cannot verify. Both sides can accept `@alex` and both are locally correct.Either signup is unavailable on one side, or you accept duplicates and rename someone afterwards — which is a real product decision, not a bug.
Account balance never goes negativeassumptionYes, with escrowSplit the balance in advance: €600 spendable in Frankfurt, €400 in Virginia. Each region enforces its own share locally, so the global invariant holds without any communication.A user with €1,000 may be refused a €700 purchase on the side holding €600. The invariant is preserved; some legitimate operations are refused.
100 seats, each sold onceassumptionYes, with partitioned inventoryAllocate 60 seats to Frankfurt and 40 to Virginia before the partition. Overselling becomes impossible.Underselling becomes possible: Frankfurt sells out while Virginia holds 40 unsold seats it cannot transfer.
Same partition, five invariants, five different answers

The trade is always the same shape

Look at what the last two rows did. They did not weaken the invariant and they did not coordinate. They pre-partitioned the resource so that each region holds an exclusive share it can enforce alone. This is the single most useful technique in the module, and it generalises: escrow for money, allocation for inventory, per-region id ranges for sequence numbers, per-region quotas for rate limits.

The price is always the same and it is worth naming precisely: you convert "sometimes wrong" into "sometimes unnecessarily refused". Overselling becomes impossible; underselling becomes possible. Overdrafts become impossible; false declines become possible. For most businesses that is an excellent trade, because a refusal is visible, explicable and recoverable while a violated invariant is silent and expensive — but it is a trade, and the product owner should make it, not the database.

When the resource cannot be split — a globally unique name over an unbounded namespace, a strict global sequence, a "at most one active session" rule — there is no escape. One side must stop, or you must repair afterwards. [[distributed-uniqueness]] is the general treatment, and [[coordination-avoidance]] is the discipline of noticing which of your invariants are actually splittable, which is more of them than people assume.

The asymmetry you can buy: a witness

A two-region partition is symmetric, and symmetry is the problem: both sides have equal claim, so any rule of the form "the survivor keeps writing" is unimplementable, because both believe they are the survivor.

A third location breaks the symmetry cheaply. Put a witness — a small, stateless-ish voter holding no user data — in a third region. Now a partition has an asymmetric outcome: whichever side can still reach the witness holds the majority and keeps writing; the other side steps down. This is [[quorums]] applied to geography, and it converts an undecidable situation into a decidable one for the cost of one small deployment.

The honest costs. The witness is now on the availability path for promotion decisions, so its own outage matters. It adds an RTT to any decision that consults it, which is why it is used for *leadership* decisions rather than per-write ones. And it must be in a genuinely independent failure domain — a witness in the same cloud provider’s shared control plane is less independent than the diagram suggests, and a partition that takes out the provider’s global services takes the witness with it.

Three full regions do the same job while also giving you capacity, which is why the standard advice for anything needing consensus across geography is three regions rather than two. Two regions plus a witness is the budget version, and a legitimate one.

The healing is where the bugs actually surface

People plan for the partition and forget the merge. But nothing goes wrong *during* a partition — each side is internally consistent and locally correct the entire time. Everything goes wrong when the link returns and two internally-consistent histories have to become one.

The reconnection is also an operational event in its own right: hours of queued replication arrive at once, saturating the link, the apply path and often the database. A partition that lasted forty minutes can produce an hour of degraded performance after it ends, and teams frequently misread that as a second incident. Rate-limit the drain.

Then the actual merge. Duplicate unique values must be resolved and somebody is getting renamed. Escrow shares must be rebalanced, and any region that exhausted its share while another sat idle needs to be replenished. Counters must be summed rather than compared. Ordering-dependent state machines need to be replayed by causality rather than by timestamp. And every one of these is an *application* decision — [[application-merge]] — because the database cannot know that the correct resolution for two conflicting shipping addresses is to ask the customer.

Which yields the most practical instruction in this lesson: write the merge procedure before the partition, not after. For each dataset that both regions may write, there should be a named, tested answer to "what happens when these two histories meet?" A system where that answer does not exist has not chosen availability under partition — it has chosen to discover its merge semantics during an incident.

Divergence, and the moment it has to be resolvedsimplified
Frankfurt is down over this spanFrankfurtVirginiareplicate signup: sent, never arrives — dropped in flightreplicate signupdropped — never arrivesreplicate signup: sent, never arrives — dropped in flightreplicate signupdropped — never arrivesbacklog drains: deliveredbacklog drainslink fails — last message received (crash) at t=0link fails — last message receivedaccept signup @alex (write) at t=3accept signup @alexaccept signup @alex (write) at t=4accept signup @alexlink restored (recover) at t=9link restoredtwo @alex rows — application must decide (decide) at t=11two @alex rows — application must decidet=0time →t=11
delivereddelayed (dashed, long)duplicated (×2)dropped — stops short, never arriveswritecrashrecoverdecide
Neither region did anything wrong. Both enforced uniqueness against everything they could see. The violation is created by the merge, not by either write — which is why the fix has to live in the merge and in the product, not in the write path.

Key points

  • Both regions healthy, mutually unreachable, both serving: this is the scenario, and it does not look like an outage on any dashboard.
  • Neither side can tell whether it is the majority or the minority — the two are identical from the inside.
  • Whether both may keep writing is decided per invariant, not per system, and the same system will have different answers for different tables.
  • Record-local invariants with a single owner survive; global aggregates survive only if pre-split; uniqueness over an open namespace never survives.
  • Escrow and pre-allocation convert "sometimes wrong" into "sometimes unnecessarily refused" — usually an excellent trade, always a product decision.
  • A witness in a third location breaks the symmetry and makes the situation decidable; three regions do it better.
  • Nothing breaks during the partition. Everything breaks at the merge, which is why the merge procedure must exist beforehand.

The chain, answered

Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.

How it works
  • Each region detects that the peer has stopped responding and waits out a flap window before treating it as a partition.
  • Each region consults its policy per dataset: continue writing, degrade to read-only, or continue within a pre-allocated share.
  • Writes proceed against the local view, which is now permanently incomplete for the duration.
  • Replication queues accumulate on both sides, bounded only by disk.
  • On restoration, backlogs drain — ideally rate-limited, since the arrival is a burst not a stream.
  • Conflicting histories are merged by the per-dataset policy: union for logs, escrow rebalancing for allocations, application resolution for genuine conflicts.
  • The invariants are asserted directly after the merge, and violations are surfaced as work items rather than errors.
What can fail at the boundary
  • The partition is partial — some traffic classes cross and others do not — so the two regions disagree about whether there is a partition at all.
  • The partition is intermittent, and each reconnection replicates a slice of state before failing again, producing interleaved histories that are harder to merge than a clean split.
  • Replication queues fill the disk on one side, and the region fails for storage reasons rather than network ones.
  • The reconnection burst saturates the link and the apply path, extending the incident well past the network repair.
  • A one-way partition: Frankfurt can reach Virginia but not vice versa, so one side sees a healthy peer and the other sees a dead one, and their policies disagree.
How it fails — what an operator sees
  • Silent divergence: both regions serve normally for forty minutes and nobody declares an incident, because the only failing signals are peer health and replication lag. The cost is discovered at the merge.
  • Duplicate identities after healing: two accounts with the same username exist. The operator sees a constraint violation in the apply stream and must choose which real customer gets renamed.
  • Drain storm: the link returns and an hour of backlog arrives at once, saturating the database and causing a user-visible latency incident *after* the network was declared fixed.
  • Disk exhaustion on the sender: the replication queue grows unbounded during a long partition and takes down a region that the partition itself had not affected.
  • Policy disagreement under a one-way partition: one region steps down to read-only while the other keeps writing, so users on the read-only side see failures while the system is, from the other side, entirely healthy.
  • Escrow starvation: one region exhausts its pre-allocated share and refuses legitimate transactions while the other side holds unused allocation it cannot transfer. The operator sees declines with plenty of global headroom.
Where coordination is required
  • None is possible during the partition — that is the definition of the situation, and every design choice here is about what to do given that.
  • All coordination must therefore happen *before* (pre-allocating shares, assigning owners, granting leases with expiry) or *after* (merging, reconciling, rebalancing).
  • A witness moves a small, decisive amount of coordination — who may lead — into a third failure domain, which is enough to break the symmetry without putting a round trip on every write.
  • Any invariant left un-factored before the partition becomes reconciliation work after it, in proportion to the write volume during the window.
What still holds under failure
  • Each region remains internally consistent and locally linearizable for the data it owns; the loss is global, not local.
  • Cross-region invariants are unenforced for the duration, whether or not that was an intentional decision.
  • Durability holds on both sides — nothing that was committed is lost by the partition itself.
  • The divergence is bounded by the partition duration times the conflicting write rate, which is the number worth estimating in advance.
How it recovers
  • Detect: alert on peer reachability and replication lag as primary signals, since neither error rate nor latency will move. Treat "replication lag rising with a healthy write path" as a page.
  • Contain: apply the per-dataset policy immediately and visibly — degrade the datasets that must not diverge to read-only, and tell users why, rather than letting the divergence accumulate silently.
  • Recover: drain the backlog under a rate limit, watching apply latency rather than queue depth, so the reconnection does not become the second incident.
  • Reconcile: run the per-dataset merges, escalating genuine conflicts to the application or to a human queue. Rebalance escrow shares. Sum counters rather than comparing them.
  • Verify: assert every invariant that the partition suspended — uniqueness, balance sums, allocation totals — and report the count of violations found and repaired as an incident metric.
How you would know
  • Peer reachability between regions as an explicit, alertable signal, not as a component of an aggregated health score where it disappears.
  • Replication queue depth and age on both sides, with disk-headroom projection: how long until the queue itself causes an outage.
  • Writes accepted per region during a partition, per dataset — the direct measure of how much reconciliation work is being created.
  • Post-merge invariant violation counts, tracked over time. A partition with zero violations tells you the pre-factoring worked.
  • Escrow utilisation per region, so starvation is visible before it becomes declines.
When it helps
  • Any two-region deployment: this scenario will occur, and having answered it per dataset in advance is the difference between a controlled degradation and an archaeological exercise.
  • Designs where availability genuinely matters more than a strict global invariant — and where that has been said out loud and agreed.
  • Systems with naturally partitionable resources, where escrow makes both availability and correctness achievable at once.
When it hurts
  • Deployments where the invariants were never enumerated: the policy defaults to "both keep writing", and the reconciliation debt is discovered later at an inconvenient scale.
  • Systems with heavy cross-region shared mutable state, where almost every dataset lands in the "must degrade" column and the partition is effectively an outage anyway.
  • Teams that plan for the partition but never for the merge, which is where all of the actual damage lives.
Simpler alternatives
  • Single-writer with fenced failover: the partition becomes a clean, if unavailable, situation with no divergence to merge — [[region-active-passive]].
  • Three regions with a majority quorum, so a partition has a defined winner and the losing side steps down automatically — [[quorums]].
  • Two regions plus a witness, which buys the same asymmetry for a fraction of the cost.
  • Pre-factor every constrained resource into per-region shares so the question stops being interesting — [[coordination-avoidance]].
  • Design the data to merge: append-only logs and CRDT-shaped structures make the partition genuinely harmless for the data they cover — [[crdts]].

EU and US are partitioned. Can both keep accepting writes?

EU and US are partitioned. Can both keep accepting writes?
Both regions healthy, mutually unreachable, both serving users, and neither can tell whether the other is dead or merely unreachable. This does not look like an outage on any dashboard.
regions accepting writes
3 of 3
converged?
yes, at step 2
messages dropped
0
conflicting writes to reconcile
0
Both sides healthy, both sides isolated, both sides wrong about the other.simplified
eu ↔ us: okeu ↔ ap: okus ↔ ap: okEU (Frankfurt) · leader · up — serving EU users, replication queue growingEU (Frankfurt)★ leaderUS (Virginia) · follower · up — serving US usersUS (Virginia)· followerAP (Singapore) · follower · up — serving APAC usersAP (Singapore)· follower
ok
  • EU (Frankfurt) — serving EU users, replication queue growing
  • US (Virginia) — serving US users
  • AP (Singapore) — serving APAC users
Steady state. One ordering point, no divergence.
14:02  the transatlantic link degrades and then stops
14:02  Frankfurt: "Virginia is down"      ← it is not
14:02  Virginia:  "Frankfurt is down"     ← it is not
14:0x  both accept signup @alex           ← both locally correct
14:0x  link healthy — one ordering point, no divergence
       the application must decide. There is no fourth option:
         (a) invariant holds, one side refuses writes
         (b) invariant pre-split into per-region shares, both stay available
         (c) both stay available, invariant repaired afterwards
       choosing not to decide selects (c) by default.
Nothing breaks during the partition. Everything breaks at the merge. A partition that lasted forty minutes can produce an hour of degraded performance after it ends, as the backlog arrives all at once — rate-limit the drain. And note what escrow actually trades: pre-splitting a balance or an inventory converts “sometimes wrong” into “sometimes unnecessarily refused”. Overselling becomes impossible; underselling becomes possible. Overdrafts become impossible; false declines become possible. That is usually an excellent trade and it is always a product decision.
Both keep writing?WhyWhat it costs
Append-only audit log per userprotocolYesEntries commute; there is no shared state to disagree about. The merged log is the union.Ordering between regions is undefined until merge, so any report assuming a total order is wrong during the window.
A user's profile record, one owner per userprotocolYesEach user is owned by one region, so a partition splits the users, not the records.Users whose home region is on the far side cannot write. Their region is up; their data is not reachable.
Usernames are globally uniqueprotocolNoUniqueness is a claim about absence, and absence is exactly what an isolated region cannot verify.Either signup is unavailable on one side, or you accept duplicates and rename someone afterwards.
Account balance never goes negativeassumptionYes, with escrowSplit the balance in advance: €600 spendable in Frankfurt, €400 in Virginia. Each region enforces its own share locally.A user with €1,000 may be refused a €700 purchase on the side holding €600. The invariant is preserved; some legitimate operations are refused.
100 seats, each sold onceassumptionYes, with partitioned inventoryAllocate 60 seats to Frankfurt and 40 to Virginia before the partition. Overselling becomes impossible.Underselling becomes possible: Frankfurt sells out while Virginia holds 40 unsold seats it cannot transfer.
Same partition, five invariants, five different answers. Whether both regions may keep writing is a property of the invariant, not of your preference.
simplifiedOne link failing cleanly. Real partitions are partial, asymmetric and intermittent — Frankfurt can reach Virginia but not the reverse — and each variant makes both the policy and the merge harder than the clean case shown. Convergence and divergence are computed by the platform’s gossip model over these three nodes.

What people believe, and what is true

Claim

During a partition the system picks consistency or availability.

Reality

The *system* does not pick; each operation does, and the useful granularity is the invariant. A single request can be available for one field and unavailable for another, and a good design makes that explicit rather than accidental.

Claim

The bigger side should keep serving.

Reality

Neither side can tell which it is. "Bigger" is only knowable relative to a fixed membership and a majority rule agreed in advance — which is exactly what a quorum or a witness provides, and what two symmetric regions lack.

Claim

If both regions stay up, users are unaffected.

Reality

Users are unaffected during the partition and affected at the merge — a renamed account, a reversed transaction, an edit that vanished. The impact is deferred, not avoided.

Claim

We will just reconcile afterwards.

Reality

Reconciliation is an application-level decision about which of two legitimate outcomes wins, and it often requires contacting a human. "We will reconcile" without a written per-dataset procedure means "we will improvise under pressure".

Claim

A partition between regions is rare enough to ignore.

Reality

The full link failure is rare; the *degradation* that makes peers look unreachable is routine, and it produces the same decision under the same uncertainty. Most teams have already been in this state without noticing.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

Both regions are alive and cannot see each other. Whether both may keep writing depends on the invariant: append-only data yes, single-owned records yes, global uniqueness no, divisible resources yes if you split them in advance.

Practical

Enumerate your datasets and assign each one a partition policy now: keep writing, degrade to read-only, or write within a pre-allocated share. Alert on peer reachability and replication lag, because nothing else will move. Write the merge procedure for each dataset that may diverge, and rehearse the drain — the reconnection burst is its own incident.

Advanced

The general result is that an invariant can be enforced without coordination exactly when it can be expressed as a conjunction of locally-checkable conditions — the property the literature calls invariant confluence. Append-only insertion is confluent; decrementing toward a floor is not, but *pre-splitting the floor* makes it so, which is why escrow works and is not a hack. This reframes the design task: rather than asking whether to be CP or AP, ask which of your invariants are confluent as written, which can be made confluent by factoring, and which genuinely cannot. Only the last group forces a choice, and in most systems it is a short list — typically uniqueness over an open namespace and strict global ordering. Everything else was a coordination decision made by habit.

Apply it

Build it, then break it
  • 🔧 For each dataset in one service, write down the partition policy and the merge procedure. Mark the ones where no answer exists.
  • 🔧 Take one constrained resource — a quota, an inventory, a credit limit — and design the escrow split, including how shares are rebalanced when the partition heals.
Reason about this
  • A one-way partition: Frankfurt can send to Virginia but receives nothing back. Both apply their partition policies. Describe what each region believes and what users on each side experience.
  • The partition lasts long enough that the replication queue threatens the disk. Argue for and against dropping the queue.
Interview questions
  • 💬 EU and US are partitioned and both are healthy. Can both keep accepting writes? Answer for a username registry, an audit log and an account balance.
  • 💬 Why can neither side tell whether it is the majority?
  • 💬 What does adding a witness in a third region buy you, and what does it cost?
  • 💬 The link comes back after forty minutes. Walk me through the next hour.