Geo-Distribution: Multi-Region Deployment Strategies
Deploy applications across multiple geographic regions for low latency and high availability. Covers latency-based routing, conflict resolution, and global distribution.
Geo-distribution spreads your application across geographic regions, keeping users fast regardless of location. The fundamental decision is whether all regions accept writes (active-active) or one region handles everything while others just serve reads (active-passive). When multiple regions accept writes, you need a strategy to merge conflicting changes — last-write-wins, vector clocks, and CRDTs each handle this differently. Most teams should start with a single primary and read replicas; the operational complexity of multi-primary is rarely worth it unless write latency is genuinely critical.
Geo-Distribution: Multi-Region Deployment Strategies
Introduction
Modern applications serve users worldwide. Single data center deployment stops working when your user base spans continents. Geo-distribution means spreading your application and data across multiple geographic regions—keeping things fast for everyone.
Users in Tokyo talking to servers in Virginia face 150-200ms round trips. Light takes about 55ms to cross that distance in a straight line. Fiber optics add more overhead. Users start noticing delays past 100ms. Past 300ms, things feel broken.
There are three reasons you might go multi-region: latency, survival, and compliance.
Latency matters more than engineers admit. The math is unforgiving: 200,000 km/s through fiber, physical distances, protocol overhead. You cannot beat physics.
Availability improves when a regional failure does not take down your entire product. The 2021 fire at an AWS us-east-1 data center knocked out a lot of the internet. Companies running multi-region recovered faster.
Data sovereignty is increasingly non-negotiable. GDPR, India’s DPDP Act, and similar regulations require certain data to stay within national borders. Multi-region deployment handles this naturally.
Core Concepts
Multi-region deployment requires understanding a set of foundational concepts that distinguish it from single-region architectures. These concepts shape every subsequent decision, from database topology to failover logic.
Active-Active vs Active-Passive
Understanding the difference between these two deployment models is critical for choosing the right geo-distribution strategy.
Active-Passive Architecture
In active-passive mode, one region (the primary) handles all writes. Secondary regions serve reads only and cannot accept writes. During failover, a passive region becomes active.
graph LR
subgraph "ACTIVE REGION (Primary)"
P[Primary DB] --> PR[Primary Replica]
PR --> PS[Standby Replica]
end
subgraph "PASSIVE REGION (Standby)"
S[Standby DB] --> SR[Standby Replica]
end
UserWrite -->|All writes| P
UserRead1 -->|Reads| PR
UserRead2 -->|Reads| SR
P -.->|Async Replication| S
Active-Passive characteristics:
| Aspect | Details |
|---|---|
| Write latency | High for remote users (must reach primary) |
| Read latency | Low for local users, high for remote |
| Conflict resolution | None (single writer) |
| Complexity | Lower |
| RTO | Minutes (failover time + DNS update) |
| RPO | Depends on replication lag (usually seconds to minutes) |
Use cases:
- Read-heavy workloads with occasional writes
- Regulatory environments requiring clear primary region
- Systems where write consistency is critical
Active-Active Architecture
In active-active mode, all regions accept writes. Each region replicates to others, creating a multi-primary topology.
graph LR
subgraph "REGION 1 (Active)"
A1[App Server] --> DB1[Primary DB]
end
subgraph "REGION 2 (Active)"
A2[App Server] --> DB2[Primary DB]
end
subgraph "REGION 3 (Active)"
A3[App Server] --> DB3[Primary DB]
end
DB1 -.->|Bidirectional Sync| DB2
DB2 -.->|Bidirectional Sync| DB3
DB3 -.->|Bidirectional Sync| DB1
UserWrite1 -->|Local writes| A1
UserWrite2 -->|Local writes| A2
UserWrite3 -->|Local writes| A3
Active-Active characteristics:
| Aspect | Details |
|---|---|
| Write latency | Low for all users (local writes) |
| Read latency | Low (local reads) |
| Conflict resolution | Required (LWW, VC, CRDT, or application) |
| Complexity | Higher |
| RTO | Lower (no failover needed, all regions active) |
| RPO | Depends on conflict resolution strategy |
CRDTs (Conflict-free Replicated Data Types) are data structures designed so that all replicas can concurrently apply updates in any order and still converge to the same state. Rather than requiring coordination to resolve conflicts, CRDTs encode the merge semantics directly into the data structure — for example, a grow-only counter simply takes the maximum value from each replica and sums them. This makes CRDTs particularly well-suited for active-active multi-region deployments where you want all regions to accept writes locally without waiting for coordination.
Use cases:
- Write-heavy workloads from multiple geographies
- User-facing applications requiring low latency globally
- Collaboration tools with concurrent edits
Decision Matrix: Active-Active vs Active-Passive
| Criteria | Active-Passive | Active-Active |
|---|---|---|
| Write latency from remote regions | High (150-200ms) | Low (5-20ms local) |
| Conflict resolution complexity | None | Required |
| Operational complexity | Lower | Higher |
| Cost efficiency | Better for read-heavy | Better for write-heavy |
| Data consistency | Easier to maintain | Harder to maintain |
| Regional failure impact | Traffic must shift | Load balancer handles |
| Best for | Critical data, compliance | Low latency, global users |
Managed Services Comparison
Different managed databases handle geo-distribution differently:
| Feature | Aurora Global | CockroachDB | Spanner | CosmosDB |
|---|---|---|---|---|
| Deployment model | Multi-region read replicas | Multi-region SQL | Globally distributed | Multi-region with SLA |
| Writes | Single primary region | Multi-region capable | Multi-region capable | Multi-master |
| Conflict resolution | LWW (timestamp-based) | MVCC + HLC | TrueTime (bounded uncertainty) | LWW or session |
| Consistency model | Configurable per operation | Serializable per region | External consistent | 5 consistency levels |
| Latency (writes) | ~100ms cross-region | ~50-150ms cross-region | ~100-200ms cross-region | ~10-50ms local |
| Latency (reads) | ~5-20ms local replica | ~5-20ms local | ~10-50ms | ~5-10ms local |
| Automatic failover | Yes (Aurora Global) | Yes (intra-region) | Yes | Yes (multi-region) |
| Replication method | Storage-level | Raft consensus | TrueTime + Paxos | Multi-homing |
| SLA | 99.99% global | 99.99% per region | 99.999% | 99.99% |
| Estimated cost | $$ (per replication hour) | $$$ (full distribution) | $$$$ (enterprise) | $$ (RU-based) |
Detailed comparison:
// Aurora Global: Best for AWS shops needing read scaling
// - Write latency: ~100ms cross-region
// - Automatic regional failover
// - Storage auto-replication
// - Best for: MySQL/PostgreSQL compatibility, AWS ecosystem
// CockroachDB: Best for globally consistent SQL
// - Write latency: ~50-150ms (depends on placement)
// - Distributed SQL with ACID transactions
// - Multi-region SQL support with locality-aware data
// - Best for: Compliance, strong consistency, PostgreSQL wire compatible
// Google Spanner: Best for global scale with strong consistency
// - Write latency: ~100-200ms (TrueTime overhead)
// - Unlimited scale, global transactions
// - TrueTime provides bounded staleness
// - Best for: Large-scale global applications, financial systems
// CosmosDB: Best for low-latency global reads/writes
// - Write latency: ~10-50ms (local region)
// - Multi-master with automatic failover
// - 5 consistency models selectable per query
// - Best for: Web/mobile apps, globally distributed gaming
Quorum Math: R+W>N
Understanding quorum is essential for distributed database consistency. The quorum rule ensures read and write operations overlap sufficiently to guarantee consistency.
The Formula
For a distributed database with N replicas:
- W = number of nodes that must acknowledge a write
- R = number of nodes that must acknowledge a read
Consistency guarantee: If W + R > N, you get strong consistency because read and write sets must overlap.
// Example: N=3 replicas
// If W=2 and R=2, then W+R=4 > 3
// Any read must intersect with any write in at least 1 node
const N = 3; // Total replicas
// Strong consistency: W=2, R=2
// Write: 2 nodes must acknowledge
// Read: 2 nodes must acknowledge
// W + R = 4 > 3 (strong consistency guaranteed)
function canReadAfterWrite(w, r, n) {
return w + r > n;
}
console.log(canReadAfterWrite(2, 2, 3)); // true - strong consistency
console.log(canReadAfterWrite(1, 1, 3)); // false - eventual consistency
console.log(canReadAfterWrite(3, 1, 3)); // true - but write is slow
console.log(canReadAfterWrite(1, 3, 3)); // true - but read is slow
Quorum Configurations
| Configuration | W | R | N | Consistency | Write Speed | Read Speed |
|---|---|---|---|---|---|---|
| Classic strong | 2 | 2 | 3 | Strong | Medium | Medium |
| Fast writes | 3 | 1 | 3 | Strong | Slow | Fast |
| Fast reads | 1 | 3 | 3 | Strong | Fast | Slow |
| Eventual | 1 | 1 | 3 | Eventual | Fast | Fast |
| Majority | 2 | 2 | 5 | Strong | Medium | Medium |
Concrete Examples
// Example 1: Dynamo-style eventual consistency
// N=3, W=1, R=1
// W + R = 2 which is NOT > 3
// This means reads might miss writes
// Acceptable for: logging, analytics, non-critical data
// Example 2: Strong consistency required
// N=3, W=2, R=2
// W + R = 4 > 3
// Any read after write will see the written data
// Required for: account balances, inventory, payments
// Example 3: Finance-grade consistency
// N=5, W=3, R=3
// W + R = 6 > 5
// Can tolerate 2 node failures and still read consistent data
// Required for: financial transactions, critical inventory
// Example 4: Latency-sensitive but consistent
// N=5, W=3, R=2
// W + R = 5 > 5 (equal, borderline)
// Faster reads than W=3, R=3
// Trade-off: reads might briefly miss latest write
Failure Tolerance
Quorum also determines failure tolerance:
// Maximum failures tolerable:
// Write: N - W nodes can fail
// Read: N - R nodes can fail
// Read-after-write: max(W-1, R-1) node failures
// Example: N=5, W=3, R=3
// Can tolerate 5 - 3 = 2 node failures during writes
// Can tolerate 5 - 3 = 2 node failures during reads
// Must have at least 3 nodes available for any operation
function maxFailuresTolerable(N, W, R) {
const writeFailures = N - W;
const readFailures = N - R;
return {
writeFailureTolerance: writeFailures,
readFailureTolerance: readFailures,
quorumRequirement: Math.max(W, R),
};
}
console.log(maxFailuresTolerable(5, 3, 3));
// { writeFailureTolerance: 2, readFailureTolerance: 2, quorumRequirement: 3 }
Global Distribution Models
Three basic models exist for where your data lives. The choice between them is usually a business decision that happens to have a technical answer.
Single primary region with read replicas is where most teams start. All writes land in one region, typically the one closest to your heaviest write traffic or your main data center. Read replicas spread across other regions serve local reads with low latency. This model works well when writes are infrequent relative to reads, or when write latency to a single primary is acceptable. If the primary region fails, you promote a replica and update DNS. That gap between failure and restoration is your RTO.
Multi-primary means every region accepts writes locally and replicates to all others. Writes are fast everywhere, typically 5-20ms local latency. The cost is conflict resolution. If a user in London and a user in Tokyo update the same profile within seconds of each other, your database has to merge those changes. Last-write-wins loses one update. Vector clocks detect conflicts but need application code to resolve them. CRDTs eliminate conflicts for specific data types but constrain your schema. Only go multi-primary when you have measured that write latency to a single primary is actually a bottleneck.
Partitioned splits data by region so no cross-region replication is needed for user data. EU user records stay in EU data centers. US user data stays in US infrastructure. This satisfies strict data sovereignty requirements like GDPR, India’s DPDP Act, and China’s PIPL without requiring complex replication topology. The cost is that any feature needing a global view of data becomes expensive or impossible. A “find all users named Alice” query now requires querying every regional database and merging results. Most teams end up here because compliance demands it, then discover they need workarounds for features that seemed simple.
The decision framework: start with single primary unless compliance forces your hand. Add multi-primary only when real measurements show write latency to a single primary is a genuine problem for your users. Partitioning is not a performance optimization; it is a compliance solution.
Read Replica Architectures
Read replicas are the workhorse of geo-distribution. Primary database in one region, replicas in others. Applications read from the nearest replica. Writes go to the primary.
-- Application in EU reads from local replica
SELECT * FROM orders WHERE user_id = 123
-- Returns from EU replica, latency ~5ms
-- Application in US reads from US replica
SELECT * FROM orders WHERE user_id = 123
-- Returns from US replica, latency ~5ms
The issue is read-your-writes consistency. You write to the primary in us-east-1 and immediately read from the EU replica. Replication lag—usually 100ms to several seconds—means your write might not be visible yet.
You have options: route reads of recently-written data back to the primary, use synchronous replication (costly), or accept eventual consistency for some operations.
Deployment Architectures
Getting user requests to the nearest region sounds simple. The reality is more nuanced—you must choose between DNS-based routing, network-level anycast, and client-side approaches, each with distinct trade-offs for failover speed, operational complexity, and cost.
DNS-Based Routing
GeoDNS returns an IP address based on the requester’s location. Route53, Cloudflare, and others offer this.
User in Germany → dns.getResponse() → returns IP of eu-west-1 server
User in Japan → dns.getResponse() → returns IP of ap-northeast-1 server
GeoDNS has real limitations. DNS TTLs complicate fast failover. Some users use resolvers in different countries, getting wrong-region IPs. DNS cannot account for actual network conditions.
The routing decision happens at DNS resolution time, before any TCP connection. The client gets pointed to whichever IP the resolver reports as closest. Health checks run on the DNS side, not the network path, which creates a gap: a region can fail health checks but still receive traffic if some resolvers have not yet updated.
The resolver location problem bites harder than it sounds. A user in Tokyo whose corporate DNS resolver sits in Singapore gets directed to ap-southeast-1 instead of ap-northeast-1. The user is not doing anything wrong; they are just behind a corporate proxy. GeoDNS accuracy at the city level is roughly 80%, dropping significantly in mobile and enterprise scenarios where resolver location does not match user location.
The TTL tension is the other issue. Low TTLs (under 60 seconds) let you fail over faster but add DNS query volume and risk DNS provider rate limits. High TTLs mean slower failover when a region goes down. Some corporate resolvers ignore TTLs entirely and cache for hours regardless of what you set.
Anycast Routing
CDNs use Anycast: multiple servers in different locations share the same IP address. Traffic routes to the nearest physical location based on BGP routing. This is how Cloudflare and Akamai deliver content globally.
graph TD
A[User Request] --> B[Nearest PoP]
B --> C{Is content cached?}
C -->|Yes| D[Return cached content]
C -->|No| E[Fetch from origin]
D --> F[Response]
E --> F
Anycast works well for static content. For dynamic applications, you still need regional compute.
Client-Side Routing
Modern applications sometimes route in the client. The client measures latency to multiple regions and picks the fastest. This works when you control both client and server code, like mobile apps or single-page applications.
The downside: complexity moves to the client. Debugging routing issues gets harder. You need infrastructure to collect and analyze latency measurements.
Client-side routing shifts the intelligence about where to send requests from the network layer into the application itself. The client maintains a list of region endpoints, probes them with lightweight measurements (typically HTTP HEAD requests or WebSocket pings), and builds a latency map it updates over time. On each request, it selects the region with the lowest observed latency.
This approach catches actual conditions that DNS misses. If the path from a user in Berlin to eu-west-1 is congested but the path to eu-central-1 is clear, client-side routing notices. GeoDNS would still send both users to eu-west-1 because both resolvers report Berlin location.
The operational burden is where most teams stumble. You need a telemetry pipeline that collects latency measurements from clients without bogging down request performance. You need to handle the case where a region disappears from the client is view entirely. And you need to debug why a client in Sydney is sending traffic to us-east-1 when it should go to ap-southeast-1, which requires correlating client-side measurements with server-side logs.
For mobile apps with frequent updates, client-side routing is reasonable. For web applications where users might be running stale JavaScript for days, the latency map gets stale fast. A hybrid approach is common: client-side routing for the initial region selection, with a fallback to a known-good region if measurements indicate degradation.
Conflict Resolution in Distributed Databases
Multi-primary databases give you writes everywhere but introduce conflicts. Two users in different regions update the same record simultaneously—who wins depends on the resolution strategy you choose.
Last-Write-Wins
The simplest strategy: whichever write has the latest timestamp wins. Most distributed databases use some variant of this. It is easy to implement and scales well.
The catch: “latest timestamp” assumes synchronized clocks. NTP synchronization has millisecond-level uncertainty. In a distributed system, clock skew means last-write-wins can produce unexpected results.
# Last-write-wins example
def update_user(user_id, updates):
current = db.get(user_id)
if updates['timestamp'] > current['timestamp']:
db.put(user_id, updates)
# else: discard the update
Vector Clocks
Vector clocks track the causal history of updates. Each region maintains its own counter. When regions merge, the system can determine if updates are causally related or concurrent.
graph LR
A[Region A: v=1] -->|write| B[Region A: v=2]
A -->|replicate| C[Region B: v=1,1]
B -->|replicate| C
C -->|concurrent write| D[Region B: v=2,1]
C -->|concurrent write| E[Region A: v=1,2]
Vector clocks let you detect conflicts precisely. But they grow with the number of regions and add storage overhead.
Conflict-Free Replicated Data Types
CRDTs are data structures designed to merge without conflicts. Sets, counters, registers—each has a CRDT variant that can be updated concurrently and merged deterministically.
Grow-only counters work by having each region increment its own counter. The merged value is the sum of all regional counters. No conflicts possible.
CRDTs make certain data types always-conflict-free. The trade-off is that your data model must fit a CRDT structure.
Not all data types fit neatly into CRDTs. A last-write-wins register is the simplest variant—compare timestamps, keep the latest—but it discards updates. That works fine for user preferences and session state. A grow-only counter (G-counter) works differently: each region increments its own counter, and the merged value is the max of each region’s number summed together. Convergence is guaranteed regardless of update order because regions never decrement.
An LWW-register follows the same timestamp-comparison logic but stores a full value instead of a count. Useful for anything where last-write-wins is acceptable—which is most non-financial data.
For more complex structures, the remove-wins grow-only set (G-Set with tombstones) lets you add elements from any region and remove them, with the constraint that removed elements never come back. Collaborative text editing uses RGA (replicated growable array), where each operation carries a client-generated timestamp and concurrent inserts resolve by comparing those timestamps. The ordering constraint is the hard part here: if your business logic depends on operations arriving in a specific order across regions, RGA alone won’t help.
CRDT semantics have to be baked into your data model from the start. You cannot retrofit convergence guarantees onto a regular database column. If your domain requires operations that are not commutative or idempotent across concurrent edits, CRDTs will not save you—use a different approach.
The operational overhead catches most teams off guard. CRDTs need all regions to generate monotonic identifiers (client IDs, hybrid logical clocks, or ULIDs) that survive network partitions. If two regions happen to generate the same ID for different operations, the merge silently produces wrong values. Test under partition and merge conditions before going near production.
Application-Level Resolution
Sometimes you need business logic to resolve conflicts. The database cannot know whether “address changed to NYC” should win over “address changed to LA.” Your application decides.
Write conflict handlers. When the database detects a conflict, it presents both values to your handler. The handler applies business rules and returns the resolved value.
def resolve_address_conflict(local_value, remote_value):
# Prefer the most recently verified address
if remote_value['verified_at'] > local_value['verified_at']:
return remote_value
return local_value
Data Locality and User Privacy
Data locality requirements increasingly drive geo-distribution decisions. GDPR, India’s DPDP Act, and similar regulations impose strict rules about where certain data can be stored and processed.
Architecture for Compliance
Design your data layer assuming strict regional isolation:
- User PII stays in the user’s home region
- Aggregated analytics can cross borders
- Session tokens can be global but should be cryptographically signed
- Audit logs may need to remain in jurisdiction
graph TD
subgraph "EU Region"
A[EU Users] --> B[EU Primary DB]
B --> C[EU Analytics]
end
subgraph "US Region"
D[US Users] --> E[US Primary DB]
E --> F[US Analytics]
end
B -.->|Anonymized data only| G[Global Dashboard]
E -.->|Anonymized data only| G
This architecture keeps personal data regional. The global dashboard sees only aggregates.
Cross-Region Queries
Avoid queries that span regions. A “find all users” query across EU and US databases is slow, expensive, and potentially problematic for compliance.
Instead, aggregate at the regional level and merge results. Accept that global reports will have delays. Design your application to work without cross-region visibility when possible.
Cross-region queries sound reasonable in theory. You have two databases; surely you can just query both and combine the results. The problem is latency. A query that takes 5ms against each regional database takes 10ms minimum when run in series, plus network transit between you and each region. Run them in parallel and you still pay for two network round trips plus the merge logic.
Compliance is the harder constraint for most teams. GDPR Article 28 requires that processors handling EU personal data have adequate safeguards in place. When your query pulls EU user records into a US analytics pipeline, you have just moved EU personal data outside the EU. Even if the data is encrypted in transit, the legal question of where processing occurs is not fully settled.
The practical alternative is hierarchical aggregation. Each region computes its own aggregates: regional totals, regional counts, regional top-items lists. A global query then pulls only these pre-computed aggregates, not raw records. The global result is slightly stale (it reflects regional state from a few seconds ago) but it is fast and it stays within compliance boundaries because no raw PII leaves the region.
Event sourcing helps here. If every write produces an immutable event that gets replicated to all regions, you can run a global query against a local replica of the event log without touching the primary. The replica receives events asynchronously, but it contains the full history. Your global analytics query runs against the replica and never crosses a compliance boundary.
Failover Strategies
Failover is where multi-region designs face their sternest test. A well-provisioned system with elegant read routing is worthless if it cannot recover gracefully when a region goes dark.
Reference: Multi-Region Failover Timeline Reality
Failover timelines are rarely as fast as engineers hope. The gap between “we can fail over in 5 minutes” and actual production failover is usually an order of magnitude. DNS propagation adds minutes. Human operators add minutes. Database promotion has hard floor times regardless of automation.
This scenario assumes a primary region failure with DNS-based failover. The main factors: health check intervals (10-30 seconds), DNS TTLs (60 seconds minimum), operator response (2-5 minutes during business hours, longer at 3am), and replication lag before promotion.
| Factor | Optimistic Estimate | Realistic Estimate |
|---|---|---|
| Detection + alert | 10 seconds | 30 seconds |
| Human decision | 1 minute | 5 minutes |
| DNS propagation | 2 minutes | 10 minutes |
| Database promotion | 30 seconds | 2 minutes |
| Application warmup | 1 minute | 3 minutes |
| Total | ~5 minutes | ~15-20 minutes |
The gantt chart below shows wall-clock durations for each phase.
Reference: Database Failover
Database failover in multi-region setups means promoting a replica in a healthy region to primary. Managed services handle most of this automatically: they monitor primary health, maintain a hot standby, and promote it within 30-90 seconds when the primary stops responding. Self-managed databases need tools like Patroni or custom failover scripts plus careful monitoring of replication lag before promotion.
What follows covers the failover timeline step by step, pre-promotion readiness checks, shifting application traffic, and recovering stateless versus stateful workloads.
Multi-Region Failover Timeline Reality
Many engineers underestimate how long failover actually takes. Here is a realistic timeline:
gantt
title Multi-Region Failover Timeline
dateFormat X
axisFormat %s seconds
section Detection
Health check failure detection :0, 30
Alert fires :30, 45
section Decision
On-call engineer awakens :45, 120
Incident triage and diagnosis :120, 300
Decision to fail over :300, 330
section DNS
DNS TTL expires (clients) :330, 630
Cache TTL expires (resolvers) :630, 1230
section Database
Replica promotion :330, 360
Replication catchup verification :360, 420
section Recovery
Application redirect :420, 480
Health checks pass :480, 510
section Total
Minimum realistic RTO :0, 510
Typical RTO with complications :0, 900
Realistic failover timeline breakdown:
| Phase | Duration | What Happens |
|---|---|---|
| Health check failure detection | 10-30 seconds | Monitoring system detects region is down |
| Alert and human response | 2-5 minutes | On-call paged, engineer diagnoses |
| Decision to fail over | 1-5 minutes | Business logic, verification, decision |
| DNS TTL propagation | 5-15 minutes | Clients’ cached DNS entries expire |
| Database replica promotion | 30-60 seconds | Managed service promotes replica |
| Application warm-up | 1-3 minutes | Connection pools, caches warm up |
Total Realistic RTO
10-30 minutes for most systems
The DNS propagation time is often the longest phase. Even with TTL=60 seconds:
- Corporate DNS resolvers may cache longer
- Mobile carrier DNS caches aggressively
- ISP resolver caches until TTL + jitter
Reducing Failover Time
// Strategy 1: Use health check-based routing (not DNS)
// Route53 geolocation + health checks can fail over faster
// Health checks run every 10 seconds by default
// Strategy 2: Anycast for stateless workloads
// All regions share same IP via BGP
// BGP failover happens in seconds to minutes
// Strategy 3: Active-active (no failover needed)
// All regions serve traffic simultaneously
// Failed region simply stops receiving traffic
// RTO = 0 for that region
Testing Failover with Chaos Engineering
Testing failover is critical but teams skip it because triggering failures on purpose feels risky. Chaos engineering flips this: you inject failures deliberately in a controlled setting and measure whether your system behaves as expected. The goal is to discover gaps before a real outage exposes them.
The key is defining steady state first. Steady state is not just “the service is up.” It is specific, measurable criteria like “95% of requests complete within 200ms” or “zero failed writes during the test window.” Without this baseline, you cannot tell whether a test passed or failed.
Start with single-region failure simulations in staging. Production chaos experiments belong during low-traffic windows, with explicit approval, and with a way to stop the experiment if something goes wrong. AWS Fault Injection Simulator (FIS) and LitmusChaos both support regional failure experiments with abort mechanisms.
The failure modes worth testing: primary region goes dark entirely (the scenario that matters most), network partition between two regions (tests quorum and conflict resolution), database replica promotion (measures actual RTO), and DNS failover propagation (validates whether your TTL settings actually work). If failover took 12 minutes instead of the expected 5, that is an operational insight, not a test failure.
Chaos Engineering Principles for Geo-Distribution
- Start small: Test in staging first, not production
- Define steady state: What does “healthy” look like?
- Hypothesis: “If we kill region X, then Y should happen”
- Measure: Verify your observability catches the failure
- Automate: Make failover testing part of your CI/CD
Testing Failover Scenarios
# Example: LitmusChaos experiment for regional failover
# litmus/failover-experiment.yaml
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: regional-failover
namespace: litmus
spec:
engineState: active
chaosServiceAccount: litmus-admin
experiments:
- name: pod-failure
spec:
components:
env:
# Simulate region failure by killing all pods
- name: TOTAL_CHAOS_DURATION
value: "300"
- name: CHAOS_INTERVAL
value: "60"
- name: TARGET_NAMESPACES
value: "production"
AWS Fault Injection Simulator (FIS) Examples
{
"description": "Simulate regional outage for failover testing",
"targets": {
"Account-vpc-infrastructure": {
"type": "aws:ssm:document",
"parameters": {
"DocumentName": "AWSFIS-Run-FAK-Regional-Outage",
"targets": [
{
"targetTag": {
"aws:resourceTag:environment": "production"
}
}
]
}
}
},
"actions": {
"regional-outage": {
"target": "Account-vpc-infrastructure",
"actionId": "aws:fis:inject-api-unavailable",
"parameters": {
"duration": "PT10M",
"services": ["ec2", "rds"],
"region": "us-east-1"
}
}
},
"stopConditions": [
{
"source": "aws:cloudwatch:alarm",
"alarmName": "FailoverSuccess"
}
]
}
Failover Testing Checklist
#!/bin/bash
# failover-test.sh - Pre-requisites before running failover test
# 1. Verify monitoring is catching failures
echo "Checking alert routing..."
curl -s http://monitoring/alerts | jq '.active[] | select(.severity=="critical")'
# 2. Verify RTO measurement
echo "Starting RTO measurement..."
export FAILOVER_START=$(date +%s)
# 3. Verify backup integrity
echo "Checking latest backup..."
aws rds describe-db-snapshots --db-instance-identifier production-primary
# 4. Verify replication lag is low
echo "Checking replication status..."
psql -h primary.internal -c "SELECT extract(epoch from now() - pg_last_xact_replay_timestamp());"
# 5. Document expected behavior
echo "EXPECTED: All writes should route to eu-west-1 within 5 minutes"
echo "EXPECTED: Read latency may spike to 500ms during failover"
echo "EXPECTED: 2-3 users may see errors during DNS TTL propagation"
What to Validate During Failover Tests
| Check | Expected Value | How to Verify |
|---|---|---|
| RTO | < 30 minutes | Time from failure to 95% traffic serving |
| RPO | < 5 minutes | Data loss measured in replication lag |
| Alert time | < 2 minutes | Time from failure to alert fired |
| DNS failover | < 15 minutes | Time for all traffic to route to new region |
| Database promotion | < 2 minutes | Time for replica promotion |
| Application health | < 5 minutes | Time for app to serve traffic in new region |
| User-facing errors | < 10 minutes | Count of users seeing errors |
Multi-region deployment only helps if you can actually fail over when a region goes down.
Database Failover
With a primary-replica setup, failover means promoting a replica to primary. The challenge: promotion must be fast, replicas must be nearly current, and your application must discover the new primary quickly.
Most managed databases (RDS Multi-AZ, Aurora Global) handle failover automatically. For self-managed databases, you need tools like Patroni or custom failover logic.
-- Checking replication lag before failover
SELECT EXTRACT(EPOCH FROM now() - pg_last_xact_replay_timestamp()) AS lag_seconds;
-- If lag < 5 seconds, safe to promote
Application Traffic Failover
When a region fails, you must redirect traffic. This works through DNS updates or anycast rerouting.
DNS failover means lowering TTLs to 60 seconds or less. When you detect failure, update DNS to point to the healthy region. Users get the new IP on next resolution.
The problem: cached DNS entries. Some users will continue trying the failed region until their resolver’s TTL expires. Expect 2-5 minutes of partial outage during failover.
Stateless Application Recovery
If your application is stateless (sessions in Redis, no local storage), failover is straightforward. Spin up instances in the healthy region, update routing, done.
Stateful applications require more thought. WebSocket connections must be reestablished. In-flight requests must be retried. Consider connection pooling with automatic reconnection.
Capacity Planning for Multi-Region Deployments
Sizing regions correctly prevents the twin extremes of overprovisioning (wasting money) and underprovisioning (risking outages during traffic spikes or failover events).
Traffic Estimation
Before deploying multi-region, estimate traffic distribution:
// Estimate regional traffic percentages
const regions = {
"us-east-1": { percentage: 0.4, users: 8000000 },
"eu-west-1": { percentage: 0.35, users: 7000000 },
"ap-southeast-1": { percentage: 0.25, users: 5000000 },
};
// Calculate expected requests per region per day
const requestsPerUserPerDay = 15;
const avgRequestSizeKB = 50;
Object.entries(regions).forEach(([region, data]) => {
const dailyRequests = data.users * requestsPerUserPerDay;
const dailyGB = (dailyRequests * avgRequestSizeKB) / (1024 * 1024);
console.log(
`${region}: ${dailyRequests.toLocaleString()} req/day, ${dailyGB.toFixed(1)} GB/day`,
);
});
Compute Sizing
Each region needs enough instances to handle:
- Expected peak traffic
- Failover load from other regions
- Buffer for growth (typically 30%)
function calculateRegionCapacity(params) {
const {
peakRPS,
avgLatencyMs,
failoverMultiplier = 1.5,
growthBuffer = 1.3,
} = params;
const requestsPerSecond = peakRPS * failoverMultiplier * growthBuffer;
const msPerRequest = avgLatencyMs;
const concurrentRequests = (requestsPerSecond * msPerRequest) / 1000;
const instancesPerRegion = Math.ceil(concurrentRequests / 100); // assume 100 concurrent per instance
return {
requiredInstances: instancesPerRegion,
peakRPSWithBuffer: requestsPerSecond,
concurrentConnections: concurrentRequests,
};
}
const sizing = calculateRegionCapacity({
peakRPS: 10000,
avgLatencyMs: 150,
failoverMultiplier: 1.5,
growthBuffer: 1.3,
});
console.log(`Required instances per region: ${sizing.requiredInstances}`);
Database Sizing
Cross-region replication adds overhead. Size your database capacity accounting for:
- Write volume per region
- Replication bandwidth requirements
- Connection pool sizing per region
-- Estimate connection pool size per region
-- Based on: concurrent users * requests per second * average session duration
SELECT
region,
active_connections,
max_connections,
ROUND(active_connections::numeric / max_connections * 100, 2) AS utilization_pct
FROM pg_stat_database
WHERE datname = 'production';
Capacity Planning Checklist
Capacity planning for multi-region deployments is where theoretical architecture meets operational reality. Getting this wrong means either paying for idle infrastructure or scrambling during traffic spikes. Work through these items before you go live:
- Map traffic by geography using existing analytics. You cannot size regions without knowing where your users are. Pull the last six months of traffic logs and build a geographic distribution map.
- Calculate peak traffic per region, including failover scenarios. When us-east-1 fails, eu-west-1 absorbs its traffic. Size eu-west-1 for base load plus the highest single-region failover load, not just its own organic peak.
- Size compute instances with 30% growth buffer. Infrastructure procurement takes time; you want headroom without overprovisioning. The 30% buffer handles both growth and the sudden surge that comes when another region fails.
- Size database storage with replication factor. Replicas need as much storage as the primary. If your primary holds 1TB, each replica needs 1TB. Account for this before you provision.
- Estimate cross-region replication bandwidth. Write-heavy workloads generate significant replication traffic. A 10,000 RPS application writing 1KB per write generates roughly 10MB/s of replication traffic—every second, continuously.
- Plan for regional concentration during off-peak hours. When US sleeps, EU carries more load. When Asia sleeps, both US and EU are active. Model these patterns explicitly.
- Test load handling before going live. Staging environments rarely match production traffic patterns. Use chaos engineering to validate actual capacity limits.
Network Topology and Latency Considerations
Geo-distribution performance hinges on network topology. Understanding the underlying network helps you design better.
Internet Backbone Latency
Traffic between regions traverses internet backbone lines. These have predictable latency characteristics:
// Typical backbone latencies (one-way, ms)
const backboneLatency = {
"us-east-1 to eu-west-1": 70,
"us-east-1 to ap-southeast-1": 180,
"eu-west-1 to ap-southeast-1": 150,
"us-west-1 to ap-northeast-1": 100,
"eu-west-1 to us-west-1": 150,
};
// Calculate round-trip times
Object.entries(backboneLatency).forEach(([path, oneWay]) => {
console.log(
`${path}: ${oneWay * 2}ms RTT (realistic: ${oneWay * 2 + 30}ms with overhead})`,
);
});
Private Link vs Public Internet
For cross-region replication, private links reduce latency and improve security:
| Factor | Public Internet | Private Link (Direct Connect/Peering) |
|---|---|---|
| Latency | Variable (10-30ms overhead) | Predictable (5-10ms overhead) |
| Bandwidth | Shared, metered | Dedicated, consistent |
| Security | TLS required | Additional layer of protection |
| Cost | Per GB transfer | Fixed hourly + per GB |
| Reliability | Variable | SLA-backed |
Global Load Balancing Deep Dive
Global load balancing determines how user traffic reaches your infrastructure and how gracefully it reroutes during regional failures. The choice of strategy affects latency, availability, and operational complexity.
Anycast vs Geolocation DNS
Anycast announces the same IP from multiple regions. The internet routes users to the nearest announced location via BGP. This is how CDNs achieve low latency globally—users automatically use the closest edge server.
Geolocation DNS returns different IPs based on the user’s reported location. A user in Germany gets the EU-west-1 IP; a user in Japan gets the ap-northeast-1 IP. Route53 and Cloudflare both offer this.
| Factor | Anycast | Geolocation DNS |
|---|---|---|
| Failover | Automatic (BGP reroutes) | Manual (update DNS records) |
| Latency | Optimized by network routing | Optimized by geographic distance |
| Precision | Coarse (internet routing path) | Fine (user-reported location) |
| Complexity | High (requires network setup) | Low (DNS configuration) |
| Static content | Excellent | Good |
| Dynamic apps | Limited (no session affinity) | Good (full control) |
Health-Check-Based Routing
Beyond DNS and Anycast, health-check-based routing provides the most control:
// Health check configuration example
const regionHealth = {
"us-east-1": { status: "healthy", latency: 45 },
"eu-west-1": { status: "degraded", latency: 120 },
"ap-southeast-1": { status: "healthy", latency: 85 },
};
function routeRequest(userRegion) {
const healthy = Object.entries(regionHealth)
.filter(([, state]) => state.status === "healthy")
.sort((a, b) => a[1].latency - b[1].latency);
if (healthy.length === 0) {
throw new Error("No healthy regions");
}
// Route to lowest latency healthy region
// Fall back to user's home region if others are unhealthy
return healthy[0][0];
}
Session Affinity in Global Load Balancing
Session affinity—sometimes called sticky sessions—routes a user to the same region for some or all of their session. The goal is to keep the user hitting the same backend servers so cached data stays valid and TCP connections stay warm. Without affinity, a user might write in us-east-1 and read from eu-west-1 on the next request, introducing the read-your-writes consistency problem described earlier.
Implementing affinity depends on your routing layer. The simplest approach uses a cookie or JWT claim that encodes the user’s home region. Your global load balancer reads this, ignores geographic routing rules for this user, and forwards them to their assigned region. This works with both DNS-based and anycast routing.
| Affinity method | How it works | Best for | Trade-off |
|---|---|---|---|
| Cookie-based | LB reads region=eu-west-1 cookie, overrides geo routing | Web apps with session cookies | Cookie must be set on first request; sensitive to SameSite policy |
| JWT claim | Token contains home_region claim; validated at app layer | APIs, SPAs, mobile apps | Requires re-issuance when region changes; claim must be trusted |
| IP-based | Hash of client IP maps to region | Edge cases where cookies unavailable | Inaccurate for mobile users behind NAT; VPNs cause misrouting |
| Global LB rule | Override routing for matched users | Enterprise SSO scenarios | Adds complexity to LB config; hard to debug |
Affinity helps most when your application does significant read-your-writes within a session. A user checking their profile after updating it, a shopper seeing items they just added to cart—these benefit from staying in the same region. Social feeds and analytics dashboards where fresh data matters less can tolerate cross-region reads.
The failure mode bites when the home region goes down. The affinity cookie still points to the dead region; the LB has to detect this and override. Without that logic, affinity turns into a liability during failover—users queue up for a region that cannot serve them. Health-check-based routing handles this gracefully; pure cookie affinity does not.
graph LR
U[User] --> LB[Global LB]
LB -->|sticky| R1[Region 1]
R1 -->|cache hit| U
R1 -.->|cache miss| R2[Region 2]
R2 -.->|origin fetch| U
One more consideration: affinity is not all-or-nothing. You can route reads to the local replica for performance but route writes to the primary regardless of home region. This gives you local read latency while preserving write consistency. The complexity is in your application logic, not your routing—each read request must decide whether to hit the local replica (risking stale data) or route to primary (adding latency). For high-value operations like viewing an order confirmation, the primary is worth the cross-region round trip. For browsing a product catalog, a local replica with brief staleness is acceptable.
Designing for Network Partitions
Network partitions between regions will happen. Design for it:
// Partition detection and handling
class RegionPartitionHandler {
constructor(regions) {
this.regions = regions;
this.partitionStatus = new Map();
}
detectPartition(sourceRegion, targetRegion) {
const key = `${sourceRegion}->${targetRegion}`;
// In practice: measure latency and packet loss
// If latency > threshold or packet loss > 5%, assume partition
return this.partitionStatus.get(key) || false;
}
getWriteableRegions(currentRegion) {
return this.regions.filter((region) => {
if (region === currentRegion) return true;
return !this.detectPartition(currentRegion, region);
});
}
// When partitioned: favor availability or consistency?
// This is your CAP theorem choice in code
chooseMode() {
// Most applications: availability
// Financial systems: consistency
return "availability"; // or 'consistency'
}
}
Latency Budget
Allocate your latency budget across components:
const latencyBudget = {
total: 200, // ms - acceptable end-to-end latency
breakdown: {
"DNS + TLS": 30,
"Load balancer": 5,
"Application compute": 50,
"Database read (local)": 20,
"Database write (cross-region)": 80,
"Network transit": 15,
},
};
// Verify budget allocation
const allocated = Object.values(latencyBudget.breakdown).reduce(
(a, b) => a + b,
0,
);
console.log(
`Budget: ${latencyBudget.total}ms, Allocated: ${allocated}ms, Remaining: ${latencyBudget.total - allocated}ms`,
);
Cache Invalidation Strategies in Geo-Distributed Systems
Caching becomes complex when users and data span regions. A stale cache in one region can serve outdated data while the primary region has already been updated—consistency violations that users notice.
The Invalidation Problem
When you write in region A and read from region B, the cache in region B might still hold stale data:
sequenceDiagram
participant User as User (Region B)
participant CacheB as Cache (Region B)
participant DB as DB Primary (Region A)
participant CacheA as Cache (Region A)
User->>CacheB: Read user:123
CacheB->>User: Return cached (stale!)
Note over CacheB: Data from 2 minutes ago
User->>CacheA: Write user:123 update
CacheA->>DB: Update
CacheA->>CacheB: Invalidate? (too slow, skip)
Invalidation Strategies
Write-through caching: Updates cache on every write. Ensures consistency but adds latency to writes.
Write-behind caching: Updates cache asynchronously after write succeeds. Lower write latency but brief inconsistency window.
TTL-based expiration: Caches expire automatically. Simpler but allows stale reads.
Active invalidation: Write triggers invalidation to all regional caches. Most consistent but requires additional infrastructure.
// Compare invalidation strategies
const invalidationStrategies = {
writeThrough: {
writeLatency: "high", // must update cache before returning
readConsistency: "strong", // always fresh
complexity: "medium",
bestFor: "read-heavy with consistency requirements",
},
writeBehind: {
writeLatency: "low", // async cache update
readConsistency: "eventual", // brief staleness window
complexity: "medium",
bestFor: "write-heavy workloads",
},
ttl: {
writeLatency: "very low", // no cache interaction on write
readConsistency: "eventual", // stale until TTL expires
complexity: "low",
bestFor: "non-critical data",
},
activeInvalidation: {
writeLatency: "medium", // must send invalidation
readConsistency: "strong", // all caches invalidated
complexity: "high", // requires pub/sub infrastructure
bestFor: "strict consistency requirements",
},
};
Regional Cache Architecture
graph TD
subgraph "User Traffic"
UE[EU Users]
US[US Users]
UAP[APAC Users]
end
subgraph "Regional Edge Caches"
CE[EU Cache]
CS[US Cache]
CAP[APAC Cache]
end
subgraph "Origin"
APP[Application Servers]
DB[(Primary DB)]
end
UE --> CE
US --> CS
UAP --> CAP
CE --> APP
CS --> APP
CAP --> APP
APP --> DB
Cache Key Design for Geo-Distribution
Include region in cache keys when data is regionally partitioned:
// Good: region-specific cache keys
const cacheKeys = {
userProfile: (userId, region) => `user:${userId}:${region}`,
productCatalog: (productId) => `product:${productId}`, // global
userSession: (sessionId) => `session:${sessionId}`, // global
};
// Avoid: assuming single global cache for user-specific data
// const BAD_KEY = `user:${userId}`; // will cause cross-region stale reads
Cost Considerations
Multi-region deployment is not cheap. You pay for data transfer between regions, additional compute capacity, and operational complexity.
Data Transfer Costs
Cross-region data transfer is one of those costs that starts negligible and grows silently until it appears on your bill looking wrong. AWS egress pricing between us-east-1 and eu-west-1 runs $0.02 per GB. Between us-east-1 and ap-southeast-1 it is $0.09 per GB. Google Cloud and Azure have similar pricing with minor variations. These numbers sound small until you run the math on a busy application.
A write-heavy application generating 5MB of replication traffic per minute per region pair works out to roughly 7,200 GB per month. At $0.02 per GB that is $144 per month per region pair. Add a third region and you pay that $144 twice over. A larger application writing 100GB per day of replication data generates about 130TB monthly, which comes to $2,600 per month in cross-region egress, just for replication, before you count user traffic.
The first optimization: write locally when possible. Route writes to a local primary and replicate asynchronously. Replication traffic is usually far less than user-facing traffic because you can batch and compress it.
Storage-level replication like Aurora Global and Spanner compresses and deduplicates automatically, so you get efficient replication without configuration. Logical replication like PostgreSQL logical decoding and MySQL binlog gives you more control. You can filter columns to replicate only what changed, compress the stream, and batch events. Change data capture tools like Debezium emit only the changed columns rather than full row images, which can cut replication volume by 70-90% compared to full-row replication.
Private links like AWS Direct Connect, Google Cloud Interconnect, and Azure ExpressRoute change the cost structure for high-volume replication. Instead of per-GB pricing, you pay a fixed hourly port fee plus per-GB charges that are typically lower than public internet egress. At high volumes above 50TB per month of replication traffic, private links usually cost less and provide more predictable latency. The setup takes 2-4 weeks because you are coordinating with carriers, so factor that into your launch timeline.
Measure your actual replication volume before optimizing. Most teams are surprised by how much data they are actually moving. Compression and batching typically cut costs by 30-50% without any architectural changes.
Compute Overhead
You need capacity in each region. For resilience, you want enough instances to handle failover load. If us-east-1 fails, eu-west-1 must absorb its traffic.
This means running 1.5x to 2x the compute you would need for a single region. Factor this into your capacity planning.
When to Use / When Not to Use Geo-Distribution
Use geo-distribution when:
- Users span multiple continents and latency matters for core functionality
- Regulatory requirements demand data residency in specific jurisdictions
- Business continuity requires resilience against regional outages
- You have operational maturity to manage distributed systems complexity
Do not use geo-distribution when:
- Your users are concentrated in a single geographic region
- Your team lacks experience with distributed data consistency
- Your application has tight write-synchronization requirements
- Your traffic levels do not justify the operational complexity
Trade-off Analysis
Every architectural choice in multi-region systems trades one property for another—latency vs consistency, complexity vs resilience, cost vs performance. Understanding these trade-offs prevents costly mid-design pivots.
Consistency vs Latency Trade-offs
Every multi-region system faces the same fundamental tension: you cannot simultaneously maximize consistency and minimize latency. Making writes fast usually means accepting that reads might see stale data. Making reads consistent usually means waiting for cross-region communication. The approaches below represent different points on this spectrum.
| Approach | Write Latency | Read Latency | Consistency | Availability | Best For |
|---|---|---|---|---|---|
| Synchronous replication | High | Low | Strong (linearizable) | Medium | Financial transactions, inventory |
| Asynchronous replication | Low | Low | Eventual | High | User-facing reads, social feeds |
| Quorum reads/writes | Medium | Medium | Strong | Medium | Critical data with multiple replicas |
| Single primary + replicas | High (remote) | Low (local) | Eventual (async) | High | Read-heavy with occasional writes |
Choosing between these approaches means understanding your specific tolerance for staleness versus latency. A social media app can accept five seconds of stale likes; a payment system cannot accept five seconds of stale balances.
Active-Active vs Active-Passive Trade-offs
These two deployment models represent fundamentally different operational philosophies. Active-passive keeps things simple but asks you to trust that your standby region can handle load the moment you need it. Active-active spreads the load but forces you to reckon with conflicts you would never have in a single-primary world.
| Factor | Active-Active | Active-Passive |
|---|---|---|
| Write latency | Low (local writes) | High (remote users must reach primary) |
| Conflict resolution | Required (adds complexity) | None (single writer) |
| Operational complexity | Higher (multi-master topology) | Lower (primary/replica topology) |
| Cost | Higher (all regions active) | Lower (passive region can be smaller) |
| Failover complexity | Low (no failover needed) | Higher (must promote replica) |
| Data consistency | Harder to maintain (conflicts possible) | Easier to maintain (single source of truth) |
| Regional failure impact | Limited to failed region | Traffic must shift; brief outage during failover |
The right choice depends on how much complexity your team can absorb and how sensitive your application is to write latency.
DNS Failover vs Anycast Trade-offs
DNS failover and anycast solve the same problem—getting user traffic to a healthy region—but through completely different mechanisms. DNS failover gives you control; anycast gives you speed. Understanding the trade-offs helps you pick the right tool for each situation.
| Factor | DNS Failover | Anycast |
|---|---|---|
| Failover speed | Slow (minutes, due to TTL propagation) | Fast (seconds to minutes, BGP convergence) |
| Complexity | Low (DNS configuration) | High (network infrastructure required) |
| Cost | Low (DNS hosting fees) | High (specialized network services) |
| Control | Full control over routing | Limited (relies on ISP routing) |
| Static content | Works but slow failover | Excellent (CDN-style delivery) |
| Dynamic applications | Good for planned migrations | Limited to stateless or semi-stateless |
| Geographic precision | Fine-grained (geolocation DNS) | Coarse (relies on internet routing) |
Many production systems use both: anycast for the initial connection and stateless workloads, DNS failover for stateful services that need explicit routing control.
Private Link vs Public Internet Trade-offs
Cross-region replication traffic traverses either the public internet or a private connection. The choice affects latency, cost, security, and reliability. For production systems handling significant replication volume, private links usually pay for themselves despite the upfront setup cost.
| Factor | Private Link (Direct Connect/Peering) | Public Internet |
|---|---|---|
| Latency | Predictable (5-10ms overhead) | Variable (10-30ms overhead) |
| Bandwidth | Dedicated, consistent | Shared, metered |
| Security | Additional protection layer | TLS required |
| Cost model | Fixed hourly + per GB | Per GB transfer |
| Reliability | SLA-backed | Best-effort |
| Setup time | Weeks (requires carrier engagement) | Immediate |
The setup time is worth noting: private links require carrier coordination and typically take two to four weeks to provision. Public internet is available immediately but introduces variable latency that can spike during peak congestion.
Read-your-Writes Consistency Strategies
Read-your-writes consistency is the guarantee that a user sees their own writes immediately after writing them, regardless of which region serves the read. Without explicit design, eventual consistency breaks this guarantee—a user writing in Tokyo might read stale data from a replica in Virginia that has not yet received the update.
| Strategy | Consistency | Latency Impact | Complexity | Use When |
|---|---|---|---|---|
| Sticky sessions | Strong | Low (no overhead) | Low | User-specific data, session data |
| Synchronous replication | Strong | High (waits for replication) | Medium | Financial, inventory |
| Read-your-writes markers | Strong | Medium (check version) | High | Custom application logic |
| Client-side cache invalidation | Strong | Medium | Medium | Mobile apps, SPAs |
| Read-your-writes consistency (no special handling) | Weak | Low | None | Non-critical, ephemeral data |
Sticky sessions work by routing a user back to the same region where they wrote. The simplest implementation uses a cookie or JWT claim to direct requests. Synchronous replication guarantees consistency but adds cross-region latency to every write. Read-your-writes markers use version numbers or timestamps the client sends with each read, letting the read service detect and redirect to the primary if needed.
Real-world Failure Scenarios
Theory only gets you so far. Examining how actual multi-region systems have failed reveals failure modes that purely architectural thinking misses.
Reference: Region-Level Outages
Region-level outages are the hardest failure mode for multi-region systems. Unlike a single server or availability zone failure, an entire region going dark means all its compute, storage, and networking are unreachable simultaneously. The 2021 AWS us-east-1 outage, the 2024 Google Cloud europe-west-9 incident, and the 2023 Azure Australia East failure all followed the same pattern: a control plane or power failure cascaded to impact every service in the region.
Recovery from a region outage depends on pre-built cross-region capacity. Services with active-active deployments routed around the failure automatically. Services relying on manual failover took 30 minutes to hours. What follows describes these failures and how to design around them.
The critical distinction from AZ-level failures: no amount of within-region redundancy helps. Your RDS Multi-AZ setup in us-east-1 does nothing when the entire region is unreachable. Only geographic distribution—serving traffic from a second region—provides protection. That means your application must keep working when one region disappears, which requires either active-active (all regions serve simultaneously) or an active-passive setup with enough spare capacity in the secondary region to handle the failed region’s load.
Pre-provisioning is where it falls apart for most teams. When us-east-1 goes dark, eu-west-1 picks up both its normal load and the displaced us-east-1 traffic. If eu-west-1 is already running at 70% capacity, it overloads the moment failover starts. Most teams keep regions at 70-80% utilization until they have an outage—then they provision 50% headroom. The cost is real: you are essentially paying for 2x the infrastructure you would need for a single-region deployment.
Database promotion itself takes 30-90 seconds with managed services (Aurora Global, Cosmos DB). DNS propagation after that adds another 5-15 minutes before users in the failed region actually redirect. That gap is painful—requests error out or hang for minutes while resolvers catch up. Active-active avoids this entirely: all regions keep serving, no promotion, no DNS update, no gap.
The subsections below walk through the specific failure modes you will encounter around regional outages: split-brain conditions from network partitions, replication lag spikes that blow out your RPO, cache coherence failures that serve stale data after failover, DNS routing that keeps sending traffic to the dead region, and cross-region partitions that cut regions off from each other entirely.
Reference: Cache Coherence Failures
Cache coherence failures happen when a write in one region does not invalidate cached data in another region. Users see stale reads from regional caches even though newer data exists in the primary database. This is especially visible after failover events, where the previously active region’s cache may still hold data the new primary has superseded.
Beyond cache coherence, this section covers failure scenarios that affect geo-distributed systems broadly: split-brain conditions, replication lag spikes, DNS routing edge cases, and network partitions. Each follows the same pattern - a failure that is rare within a single region but becomes likely when multiple regions are involved.
Region-Level Outages
When an entire region becomes unavailable, traffic must shift to healthy regions. The 2021 AWS us-east-1 outage knocked out many services that lacked cross-region redundancy.
What happens:
- DNS-based routing requires 5-15 minutes for full failover due to TTL propagation
- Anycast routing failover happens faster (seconds to minutes via BGP) but requires pre-configuration
- Database failover requires replica promotion (30-90 seconds for managed services)
- Application servers in failed region cannot serve traffic but may hold open connections
How to mitigate:
- Run active-active so no failover is needed
- Keep DNS TTLs at 60 seconds or below
- Pre-stage capacity in secondary regions to handle failover load
- Test failover regularly with chaos engineering
Split-Brain Scenarios
Network partitions between regions create split-brain conditions where multiple regions believe they are the primary.
What happens:
- Both regions accept writes to the same data
- Conflict resolution must merge divergent data later
- Without quorum enforcement, you risk data corruption from concurrent writes
- Application logic may behave differently in each region
How to mitigate:
- Use quorum-based reads and writes (W+R>N)
- Implement partition detection and pause writes until partition heals
- Use consensus algorithms (Raft, Paxos) for leader election
- Design application-level conflict resolution for critical data
Replication Lag Violations
Asynchronous replication lag can grow beyond acceptable thresholds during network congestion or high write throughput.
What happens:
- Read-your-writes consistency violated: writes from region A not visible in region B
- Stale data served to users who have moved or whose reads route to remote regions
- RPO increases beyond intended target
- Recovery after failure takes longer as replica catches up
How to mitigate:
- Monitor replication lag with alerts at 30 seconds (warning) and 5 minutes (critical)
- Use synchronous replication for critical data
- Route reads of recently-written data to primary region
- Implement read-your-writes markers in application logic
Cache Coherence Failures
Caches across regions can serve stale data after writes or failovers.
What happens:
- User writes in region A, reads from region B, gets stale cache hit
- Regional failover leaves caches in failed region serving stale data
- Cache invalidation messages traverse slow cross-region links
How to mitigate:
- Use write-through caching for critical data
- Implement active invalidation on writes (not just TTL expiration)
- Include region in cache keys for partitioned data
- Flush or invalidate caches during failover
DNS-Based Routing Failures
DNS routing has inherent delays and edge cases that cause failures during regional issues.
What happens:
- Long TTLs cause users to hit failed region for minutes after failure
- Some users use DNS resolvers in different geographic regions
- TTL updates must propagate through multiple resolver layers
- DDoS on DNS can prevent any routing updates
How to mitigate:
- Use health-check-based routing instead of pure DNS
- Keep TTLs low (60 seconds or below)
- Implement client-side fallback logic
- Use multiple DNS providers for redundancy
Cross-Region Network Partitions
Temporary or prolonged network connectivity issues between regions create partial failures.
What happens:
- Some writes fail while others succeed depending on region
- Quorum might be lost if partition cuts through majority
- Applications must decide: continue with stale data or fail all requests
- Partition heals but requires reconciliation of divergent state
How to mitigate:
- Design for partition tolerance: choose availability or consistency explicitly
- Use eventual consistency with clear reconciliation strategies
- Implement partition detection and circuit breakers
- Test during simulated partitions before production
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Primary region goes offline | All writes fail; reads may succeed from replicas | Promote nearest replica; update DNS; monitor replication lag before promotion |
| Replication lag spikes | Read-your-writes consistency violated; stale data served | Route reads of recent writes to primary; use synchronous replication for critical data |
| Network partition between regions | Split-brain risk; concurrent writes create conflicts | Use quorum-based reads/writes; detect partitions and force consistency |
| DNS cache poisoning | Users routed to wrong region; data integrity risk | Use short TTLs; implement health-check-based routing; DNSSEC |
| Schema migration in multi-region | Rolling migration across regions; compatibility windows | Use backwards-compatible migrations; test in staging first; have rollback plan |
| Cache incoherence | Stale cache entries served after regional failover | Implement cache invalidation on failover; use write-through caching for critical data |
Common Pitfalls / Anti-Patterns
Teams approaching geo-distribution for the first time tend to repeat the same mistakes. Recognizing these patterns early saves significant debugging time later.
Ignoring Read-your-Writes Consistency
After writing to the primary region, immediately reading from a replica can return stale data. Users see their own changes disappear. Implement sticky sessions or route critical reads to the primary for a grace period after writes.
This one is subtle enough that it bites nearly every team once. You deploy a multi-region setup, test it carefully, and everything looks great. Then a user in Amsterdam posts an update, refreshes the page, and sees the old value. They post again. Still old. They conclude the site is broken and maybe send a support ticket.
The math is not complicated. Write to us-east-1 takes 80ms to complete. Replication to eu-west-1 replica takes another 100ms minimum (cross-region fiber is fast but not instantaneous). The user refreshes 150ms after posting. The replica has not yet received the update. You have a read-your-writes violation.
The solutions are known but each carries trade-offs. Sticky sessions route the user back to us-east-1 for a read-after-write, which defeats the latency purpose of geo-distribution for any read that immediately follows a write. Reading from the primary directly adds cross-region latency to every post-then-read flow. Read-your-writes markers (the client sends a version number or timestamp with each read request, and the service checks whether the replica is caught up before returning) add complexity but preserve local read performance.
The version-marker approach is the most flexible. The client includes a claim like “I last wrote version 42” and the read service checks: is the replica at version 42 or beyond? If not, it either waits briefly for replication to catch up or redirects the read to the primary. The check adds maybe 1-2ms of overhead and keeps the user experience clean. Most teams skip this because it is not in any default stack, which means it is left as an exercise for the reader.
Over-Engineering with Multi-Primary
Multi-primary databases solve a write-latency problem most applications do not have. If your users are mostly reading, a single primary with read replicas handles most workloads. Add multi-primary only when you have demonstrated write-latency requirements that cannot be met otherwise.
The appeal is obvious. All regions accept writes. No single primary bottleneck. Write latency is local everywhere. The reality is that multi-primary introduces conflict resolution complexity that most teams underestimate severely.
When two regions modify the same user record within seconds of each other, the database presents you with a conflict. Last-write-wins silently discards one update. Vector clocks detect the conflict but require application code to resolve it. CRDTs eliminate the conflict but constrain your data model to specific structures. Every option is a trade-off, and none of them are free.
I have seen teams spend months building conflict resolution logic for a multi-primary setup that could have used a single primary with async replication. The single-primary approach would have introduced 80ms extra latency for remote writes. The team estimated their p99 write latency requirement at 100ms. They were optimizing for a problem they did not have.
Before going multi-primary, measure your actual write latency distribution. If 95% of your writes originate in one region anyway, you are adding global complexity for a marginal gain. If you genuinely have balanced write traffic from multiple continents and your users are complaining about write latency, multi-primary might be warranted. Otherwise, single primary with read replicas handles the vast majority of real-world multi-region workloads.
Neglecting Cross-Region Data Transfer Costs
Cross-region replication can become a significant cost driver. Monitor transfer volumes and optimize: compress replication streams, batch events, write locally when possible.
Cross-region data transfer is one of those costs that starts small and grows quietly. In the early stages, replication traffic is negligible. As your user base grows, write volume grows, and replication traffic grows with it. The bills do not become alarming until you are already deep into multi-region architecture and finding out that egress pricing between regions is not cheap.
AWS egress pricing between us-east-1 and eu-west-1 is $0.02 per GB. That sounds small. If your application generates 5MB of write replication traffic per minute per region pair, that is 7200GB per month, or about $144 per month per region pair. Add ap-southeast-1 into the mix and you are paying that $144 twice over. A busy application with 100GB of writes per day generates about 130TB monthly of cross-region replication traffic, which is $2,600/month just in egress.
The optimization path depends on your replication method. Storage-level replication (like Aurora’s) compresses and deduplicates automatically. Logical replication (like PostgreSQL logical decoding or MySQL binlog replication) gives you more control: you can filter columns to replicate, compress the stream, and batch events. Change data capture tools like Debezium let you emit only the columns that changed rather than full row images.
The first step is measuring what you are actually replicating. Most teams are surprised by the volume. Once you see the numbers, compression and batching typically cut costs by 30-50% without any architecture changes.
Using Long DNS TTLs
Long TTLs mean slow failover. If a region goes down, users with cached DNS entries continue hitting the failed region for minutes or hours. Keep TTLs at 60 seconds or less.
How this works: when a DNS resolver queries your authoritative nameserver, it caches the response and does not ask again until the TTL expires. A 300-second TTL means some resolvers will not check for updated records for five full minutes after you change them. Those users send traffic to your dead region during that entire window.
The trade-off is query volume. Lowering TTLs means more DNS queries hitting your nameservers. With millions of users and thousands of resolvers, a 60-second TTL translates to hundreds of thousands of queries per minute. Managed DNS providers handle this without issue; self-hosted DNS infrastructure may struggle.
Corporate resolvers add a wrinkle that is easy to miss. Many enterprise DNS setups cache responses for a minimum period regardless of what your TTL says. Some ignore your TTL entirely and apply their own fixed expiration. For these users, failover takes as long as their internal policy dictates—sometimes hours. You cannot override a corporate resolver in Singapore that decides to cache everything for 30 minutes.
The operational fix: set TTLs to 60 seconds normally, but drop them further (30 or even 15 seconds) when you know a regional event is coming, such as planned maintenance. Restore them after. This requires automation—your incident management system needs to script DNS record updates through the Route53, Cloudflare, or other provider API. The goal is to have low TTLs only when you actually need fast failover, not all the time.
Health-check-based routing can reduce your dependence on TTL propagation. Rather than waiting for DNS to update, health checks detect failure at the network layer and reroute traffic automatically. The client keeps the same IP, but the underlying routing shifts without waiting for resolver caches to expire.
Observability Checklist
-
Metrics:
- Replication lag per region (target: under 5 seconds for sync, under 60 seconds for async)
- Cross-region traffic volume and cost
- Read latency by region (P50, P95, P99)
- Write latency to primary
- DNS resolution time and cache hit rates
- Connection pool utilization per region
-
Logs:
- Log region identifier in every request trace
- Record replication events and lag measurements
- Capture conflict resolution decisions with full context
- Track failover events with timestamps and reasons
-
Alerts:
- Replication lag exceeds 30 seconds (warning) / 5 minutes (critical)
- Cross-region traffic exceeds cost threshold
- Write success rate drops below 99.9%
- DNS resolution failures spike
- Region health check failures trigger early warning
Security Checklist
- Encrypt all data in transit between regions using TLS 1.3
- Implement per-region IAM roles with minimal privilege
- Use VPC peering or private links for cross-region traffic
- Apply encryption at rest with per-region keys (not global keys)
- Audit cross-region data access patterns quarterly
- Implement network segmentation to isolate regional traffic
- Log and monitor all cross-region data transfers
- Ensure compliance with data residency requirements per region
Quick Recap
Key Bullets:
- Geo-distribution serves three purposes: latency reduction, availability improvement, and data sovereignty compliance
- Single primary with read replicas handles most use cases; multi-primary adds complexity for marginal benefit
- Read-your-writes consistency requires explicit design; eventual consistency is the default
- Conflict resolution strategies include last-write-wins, vector clocks, CRDTs, and application-level resolution
- DNS failover is simple but slow; anycast is fast but limited to static or semi-static content
Copy/Paste Checklist:
Before deploying multi-region:
[ ] Define RTO and RPO per service
[ ] Choose replication strategy (sync vs async)
[ ] Implement conflict resolution for multi-primary
[ ] Set DNS TTLs to 60 seconds or less
[ ] Test failover procedure in staging
[ ] Document regional data flows
[ ] Set up cross-region monitoring and alerts
[ ] Review compliance requirements per region
Interview Questions
Three primary drivers: latency reduction, availability improvement, and data sovereignty compliance. Latency reduction matters because the speed of light limits how fast data can travel—users in Tokyo talking to servers in Virginia will always have higher latency than users talking to servers in Tokyo.
Availability improvement comes from not putting all your infrastructure in one place. If one region fails, users in other regions continue working. The business impact of a regional outage is limited to users in that region.
Data sovereignty requirements—GDPR, India's DPDP Act, China's PIPL—mandate that certain data stay within national borders. Meeting these requirements might require keeping data in specific regions even if it adds latency.
The CAP theorem says a distributed system can provide only two of three guarantees: consistency, availability, and partition tolerance. Partitions—network failures between regions—will happen. When they do, you must choose: sacrifice consistency or sacrifice availability.
For geo-distributed systems, the choice is usually availability over consistency. Users in a region need to access data even when the network to other regions is slow. Eventual consistency lets each region continue operating, with conflicts resolved later.
Some systems need strong consistency—financial transactions, inventory management. These systems choose consistency over availability and pay with higher latency during regional partitions.
Single primary with read replicas: all writes go to one primary region. Replicas in other regions asynchronously replicate data. Simple to reason about, but writes always have primary-region latency. If the primary fails, a replica must be promoted—takes time and might lose un-replicated writes.
Multi-primary: all regions accept writes. Each region replicates to others. Writes are local—no single primary bottleneck. The complexity cost is conflict resolution: two regions might modify the same data simultaneously. Without careful design, conflicts cause data divergence.
For most applications, single primary with read replicas handles the job. Multi-primary adds marginal performance benefits for a large complexity cost. Only adopt multi-primary when you have demonstrated that write latency to a single primary is a genuine bottleneck.
Eventual consistency means data changes propagate asynchronously to all replicas. There is a window—milliseconds to minutes—where different regions might show different values for the same data. The system converges to consistency once propagation completes.
Eventual consistency is acceptable for most use cases. User profile updates, social media posts, analytics data—these are all fine with brief inconsistency windows. Users rarely notice a few seconds delay in seeing profile changes.
Eventual consistency is not acceptable when strong consistency is required: financial transactions, inventory management, session management. For these cases, synchronous replication or read-your-writes consistency guarantees are necessary.
Read-your-writes consistency means a user always sees their own writes, regardless of which region serves the read. Without explicit design, eventual consistency breaks this guarantee—a user in region B might read a stale value after writing in region A.
Techniques: sticky sessions route the user to the same region where they wrote. Synchronous replication makes writes visible everywhere before acknowledging. Read-your-writes markers—timestamps or version numbers the client sends—let the read service detect stale data. Client-side caching with invalidation on writes also helps.
The choice depends on your tolerance for complexity and latency. Sticky sessions are simplest but reduce availability. Synchronous replication adds latency but guarantees consistency. Read-your-writes markers are application-specific but flexible.
DNS failover routes users by changing DNS records—pointing the domain to a healthy region's IP address. Health checks detect failure; DNS updates propagate to users over time based on TTL. DNS failover is simple to implement but slow. Even with 60-second TTLs, full propagation takes minutes.
Anycast announces the same IP address from multiple regions. The internet routes users to the nearest region automatically. When one region fails, routers worldwide detect the change within seconds or minutes—no DNS changes needed. Anycast is fast and automatic but requires special network infrastructure.
Static content works well with anycast. Dynamic applications can use anycast for the initial connection and DNS failover for full routing control. Some systems use both: anycast provides automatic nearest-region routing, DNS failover handles planned migrations and maintenance.
Last-write-wins: whichever write happened most recently wins. Simple but can lose data. Uses timestamps that might not be synchronized across regions. Only acceptable for data where occasional loss is tolerable.
Vector clocks: track the causal history of each object. When conflicts occur, the system can detect whether one write happened after another or if they were truly concurrent. Allows application-specific conflict resolution.
CRDTs (Conflict-free Replicated Data Types): data structures mathematically designed to merge concurrent changes without conflict. G-counters, OR-sets, LWW-registers—each handles specific data types. Using the right CRDT eliminates conflicts entirely for supported types.
Application-level resolution: detect conflicts and surface them for human resolution or apply business rules. Highest flexibility, highest complexity. Necessary when conflicts require business context to resolve.
Data residency regulations specify where certain data must be stored and processed. GDPR requires personal data of EU citizens to stay within the EU or in countries with adequate data protection. India's DPDP Act has similar requirements for Indian user data. China restricts data leaving Chinese borders entirely.
Architecture implications: user PII must remain in the specified region. Aggregated or anonymized data might cross borders. Audit logs might need to stay in jurisdiction. Session tokens can be global but might require cryptographic signing that allows validation without data leaving the region.
Design for strict regional isolation from the start. When data crosses borders accidentally, compliance fails. Use region-scoped databases, region-specific encryption keys, and network policies that prevent cross-region data transfer for restricted data types.
A CDN serves static content—images, videos, JavaScript, CSS—from edge locations close to users. Users in Europe get content from European edge servers, not your origin in Virginia. Latency drops dramatically for static asset delivery.
CDNs also absorb traffic spikes. Rather than hitting your origin with millions of requests, the CDN serves from cache. This protects your origin from traffic floods whether from organic growth or DDoS attacks.
CDNs are not a replacement for geo-distribution of your application servers. They handle static content. Your dynamic application servers still need to be close to users if response latency matters. Use CDNs for static assets; use geo-distribution for dynamic application servers.
Data replication lag means different regions might briefly show different data. Monitoring becomes more complex—metrics from multiple regions must be correlated. Deployment must coordinate across regions or tolerate cross-region version differences during rollout.
Network partitioning between regions happens. When it does, you must decide: should regions continue serving stale data, or should they fail? This is the CAP theorem trade-off in practice. Make these decisions explicitly before partitions happen.
Operational complexity compounds with each additional region. Each region needs its own monitoring, alerting, backup, security hardening, and compliance validation. Start with two regions; move to more only when operational maturity and tooling support it.
The quorum rule ensures read and write operations overlap sufficiently to guarantee consistency. For N replicas, if W nodes must acknowledge writes and R nodes must acknowledge reads, then W+R>N ensures that any read set overlaps with any write set in at least one node.
For example, with N=3, W=2, R=2: any read must contact at least 2 nodes, and any write must be acknowledged by 2 nodes. These sets must overlap in at least one node, so a read will see a completed write.
The trade-off is latency and availability. Higher W or R means more nodes to contact, increasing latency but reducing the window for inconsistency.
For write-heavy workloads, you need to minimize write latency. Active-active architecture lets all regions accept writes locally, then replicate asynchronously. This requires conflict resolution—last-write-wins for simple cases, vector clocks or CRDTs for more complex data.
Key considerations: choose your conflict resolution strategy before designing the schema. Use idempotent operations so retries during replication do not cause duplicates. Monitor replication lag closely; writes in one region might not be visible in others for seconds to minutes.
Alternatively, use a single primary with very fast replication if strong consistency matters more than write latency. Aurora Global and Spanner offer regional primaries with synchronous replication to a few secondaries.
Cross-region data transfer is expensive—$0.02-0.09 per GB depending on regions. Strategies: write locally when possible, batch replication events to reduce overhead, compress replication streams, and keep read-heavy workloads on local replicas.
For read replicas, async replication is cheaper than synchronous. Use multi-region read replicas in AWS Aurora or Cosmos DB multi-region for read-heavy workloads with acceptable eventual consistency.
Private links (Direct Connect, VPC peering) have fixed hourly costs plus per-GB charges—better for high-volume replication than public internet which charges per GB.
During a network partition between regions, CAP theorem forces a choice: consistency or availability. Most applications choose availability—regions continue serving requests even if they might have stale data. This is why eventual consistency is common in geo-distributed systems.
Financial systems often choose consistency—during partitions, they might reject writes rather than risk diverging data. This manifests as slower service or errors during regional outages but prevents the harder problem of reconciling conflicting transactions later.
The CAP choice is not binary at the system level. Many databases let you choose consistency per operation. A shopping cart might accept writes locally during partition (availability), while inventory checks require synchronous confirmation (consistency).
Critical metrics: replication lag per region (target under 5 seconds for sync, under 60 seconds for async), cross-region traffic volume and cost, read latency by region (P50, P95, P99), write latency to primary, DNS resolution time and cache hit rates, and connection pool utilization per region.
Alerts should trigger on: replication lag exceeding 30 seconds (warning) or 5 minutes (critical), cross-region traffic exceeding cost threshold, write success rate dropping below 99.9%, DNS resolution failures spiking, and region health check failures triggering early warning.
Logs must include region identifier in every request trace, record replication events with lag measurements, capture conflict resolution decisions with full context, and track failover events with timestamps and reasons.
Synchronous replication: the primary waits for acknowledgment from replicas before confirming the write to the client. If a replica fails to acknowledge in time, the write fails. This guarantees that data exists on multiple nodes before returning success, offering strong consistency but higher write latency.
Asynchronous replication: the primary acknowledges the write immediately after persisting locally, then replicates to replicas in the background. Writes complete faster but there is a window where data exists only on the primary. If the primary fails before replication completes, data loss occurs.
Most geo-distributed systems use async replication for cross-region writes because the latency of waiting for cross-region acknowledgment would be unacceptable. Synchronous replication is typically used within a region for high-consistency requirements.
Vector clocks assign a timestamp vector to each version of an object. Each region maintains its own counter in the vector. When a write happens in a region, that region's counter increments. When regions synchronize, they merge vectors by taking the maximum of each counter.
This merging reveals causal relationships: if all counters in one vector are less than or equal to another's, the first happened causally before the second. If some counters are greater and others lesser, the events were concurrent—neither caused the other.
Concurrent versions require conflict resolution. The application can then apply rules: merge values, pick one, or surface the conflict for manual resolution. Vector clocks enable this precise detection without relying on synchronized clocks.
Consensus algorithms like Raft and Paxos ensure all replicas agree on the same value for data, even when some replicas fail or network partitions occur. They solve the "split-brain" problem where different regions might independently decide they are the primary.
In geo-distributed contexts, consensus becomes challenging because regions might be partitioned from each other. Quorum-based reads and writes (W+R>N) provide a form of consensus without a central leader. More formal consensus algorithms use a leader elected from a quorum of regions.
Spanner uses Paxos with TrueTime for globally consistent transactions. CockroachDB uses Raft for its distributed SQL layer. These algorithms guarantee linearizability—operations appear to happen in a global order—even across regions.
RTO (Recovery Time Objective): how long it takes to restore service after a failure. In multi-region deployments, RTO includes detection time, human decision time, DNS propagation, and replica promotion. Realistic RTO is 10-30 minutes even with automation.
RPO (Recovery Point Objective): how much data loss is acceptable. Determined by replication strategy: synchronous replication achieves RPO near zero, async replication has RPO equal to replication lag (seconds to minutes).
The key insight: RTO and RPO are independent. You can have RPO=0 with high RTO (synchronous replication but slow failover) or RTO=5 minutes with RPO=1 hour (async replication with fast failover). Design each service's RTO and RPO independently based on business requirements.
Chaos engineering deliberately injects failures—region outages, network partitions, database failures—to validate that systems behave as expected. Tools like AWS Fault Injection Simulator (FIS) and LitmusChaos let you simulate regional failures safely.
Start with defining steady state: what does healthy look like? Then form hypotheses like "if region A fails, traffic should route to region B within 5 minutes with less than 1% errors." Run experiments in staging first, then production during low-traffic windows.
Critical validation points: RTO measurement (time to restore), RPO verification (data loss check), alert quality (did monitoring catch the failure?), and user impact (error rate during failover). Automate these tests in CI/CD to catch regressions before they affect users.
Further Reading
Conclusion
Geo-distribution is complex. Conflict resolution, data consistency, and operational overhead are real challenges. Before going multi-region, confirm you actually need it.
If your users span continents and latency matters, multi-region deployment solves that. The implementation choices—single primary versus multi-primary, sync versus async replication, CRDT versus application-level conflict resolution—depend on your specific requirements.
Start simple. A single primary with read replicas in two regions handles most use cases. Add complexity only when you have demonstrated need.
The patterns in this article—latency-based routing, conflict resolution, failover strategies—apply whether you use managed services or build your own infrastructure. Understanding them lets you design systems that work globally.
Category
Related Posts
CQRS Pattern
Separate read and write models. Command vs query models, eventual consistency implications, event sourcing integration, and when CQRS makes sense.
Event Sourcing
Storing state changes as immutable events. Event store implementation, event replay, schema evolution, and comparison with traditional CRUD approaches.
Amazon Architecture: Lessons from the Pioneer of Microservices
Learn how Amazon pioneered service-oriented architecture, the famous 'two-pizza team' rule, and how they built the foundation for AWS.