Multi-region Consistency Expert¶
When you'd use this
Consistency across geo-distributed regions — replication, conflicts and causality.
Serve users worldwide while managing the consistency/latency tradeoffs of replicating data across regions.
What you'll learn¶
- Why geography forces consistency tradeoffs
- The strong vs eventual consistency spectrum
- Resolve write conflicts (LWW with tie-breaks)
- Track causality with vector clocks
- Choose a consistency model per use case
Why regions change everything¶
Cross-region latency and partitions force explicit consistency choices you can ignore in one datacenter.
When your data lives in one datacenter, coordination is cheap. Spread it across continents — US, Europe, Asia — and physics intervenes: a round trip between regions is 100–300 ms. If every write had to synchronously reach every region, writes would crawl. So you replicate asynchronously, which means regions can temporarily disagree, and two regions can accept conflicting writes to the same key at the same time.
us-west ──write "blue"──┐ ┌── how do these reconcile
├─ sync ───┤ when they meet?
us-east ──write "red"───┘ └──
This is the CAP theorem made concrete: during a network partition you must choose consistency (reject writes until healed) or availability (accept writes and reconcile later). Multi-region systems usually lean toward availability, which makes conflict resolution the central problem.
The consistency spectrum¶
From strong to eventual — stronger guarantees cost latency and availability.
| Model | Guarantee | Cost |
|---|---|---|
| Strong | Every read sees the latest write, everywhere | Slow cross-region coordination (consensus) |
| Read-your-writes | You see your own writes; others may lag | Moderate |
| Eventual | Replicas converge eventually if writes stop | Fast, but reads can be stale |
There's no free lunch: stronger consistency costs latency and availability. The skill is picking the weakest model your use case can tolerate — a bank balance needs strong; a "like" count is fine eventually consistent.
Resolving conflicts: LWW with a deterministic tie-break¶
Pick a winner for concurrent writes using timestamps plus a stable tiebreaker.
The simplest conflict resolution is last-write-wins by timestamp. But two regions can produce the same timestamp — so you need a deterministic tie-breaker (like region name) so every replica independently picks the same winner. Runnable:
from dataclasses import dataclass
@dataclass
class Versioned:
value: str
ts: float
region: str # tie-breaker when timestamps are equal
def resolve(a: Versioned, b: Versioned) -> Versioned:
if a.ts != b.ts:
return a if a.ts > b.ts else b
return a if a.region > b.region else b # deterministic on equal timestamps
west = Versioned("blue", ts=100.0, region="us-west")
east = Versioned("red", ts=100.0, region="us-east") # same timestamp!
winner = resolve(west, east)
print("winner:", winner.value, "from", winner.region)
Output:
With identical timestamps, the tie-break by region name picks us-west (since "us-west" > "us-east") — and crucially, every replica computes the same winner, so they converge. Without a deterministic tie-break, two regions could each pick a different "winner" and never agree.
LWW silently discards the losing write
Whoever loses the timestamp comparison is gone. That's acceptable for a preference or a cache, but not for anything where every update must survive (inventory, financial transactions). For those, use a CRDT that merges without loss, or require strong consistency. LWW also trusts clocks — clock skew across regions can crown the wrong winner.
Tracking causality with vector clocks¶
Record per-node versions so you can tell concurrent writes from causally ordered ones.
Wall-clock timestamps can't tell whether two writes were causally related (one saw the other) or truly concurrent. Vector clocks — a counter per node — capture this. Runnable:
def happens_before(a: dict, b: dict) -> bool:
"""True if event a causally precedes b."""
keys = set(a) | set(b)
le = all(a.get(k, 0) <= b.get(k, 0) for k in keys)
lt = any(a.get(k, 0) < b.get(k, 0) for k in keys)
return le and lt
def concurrent(a: dict, b: dict) -> bool:
return not happens_before(a, b) and not happens_before(b, a) and a != b
a = {"west": 2, "east": 1}
b = {"west": 3, "east": 1} # b saw everything a did, plus one more west event
c = {"west": 2, "east": 2} # c diverged from b
print("a happens-before b:", happens_before(a, b)) # True — b descends from a
print("b concurrent with c:", concurrent(b, c)) # True — neither precedes the other
Output:
a → b because b's vector dominates a's (it saw everything and more). But b and c are concurrent — each has an event the other didn't see, so neither caused the other. That's exactly the case a system must flag as a genuine conflict needing resolution (or a CRDT merge), versus a simple overwrite. Vector clocks let you distinguish "this update supersedes that one" from "these two truly clash."
Strategies in practice¶
How real systems pick replication and conflict strategies for their needs.
- Single-writer region (leader per key). Route all writes for a key to one "home" region; other regions read a replica. Avoids write conflicts entirely, at the cost of cross-region write latency for non-home regions.
- Multi-writer + conflict resolution. Accept writes anywhere, reconcile with LWW, vector clocks, or CRDTs. Best availability, needs careful conflict handling.
- Strong consistency via consensus. Run Raft/Paxos across regions for the data that truly needs it — accept the latency.
- CRDTs. For data that fits their model (CRDTs), get conflict-free multi-region writes with no coordination — the cleanest option where applicable.
Segment your data by consistency need
Don't pick one model for everything. Put the few things that need strong consistency (payments, balances) behind consensus, and let the rest (profiles, counters, activity feeds) be eventually consistent. This is how large systems get both correctness and low latency.
Practice exercises¶
- Extend
resolveto also record which write lost, so you can log/audit discarded conflicting writes. - Implement vector-clock
merge(element-wise max) andincrement, then show a causal chain of three events. - Construct two vector clocks that are concurrent and explain, in terms of events, why neither happened-before the other.
- Design consistency choices for a social app: which data is strong, read-your-writes, or eventual, and why.
- Explain why LWW needs a deterministic tie-breaker for replicas to converge, with a concrete equal-timestamp example.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.