AWS Architecture

AWS Multi-Region Active-Active Architecture: Route 53, Global Tables and Aurora Global Database

At 03:40 on a Sunday, us-east-1 starts throwing errors — not a full outage, just enough that your checkout success rate slides from 99.6% to 71% and your pager will not stop. Your architecture diagram has a second Region drawn in it. The question that decides whether this is a five-minute event or a five-hour one is brutally simple: can us-west-2 take the write traffic right now, without a human provisioning anything, and if it does, how much data do you lose in the handoff? Almost every team that “has a DR Region” discovers at this exact moment that the second Region was a backup, not a peer — its Auto Scaling group is scaled to two tasks, its database is a read replica nobody has ever promoted under load, and its service quotas were never raised. The diagram lied.

Multi-region active-active means both Regions serve production traffic simultaneously, each a self-sufficient stateless stack, with the data layer replicating between them continuously and asynchronously. It is the top of the disaster-recovery ladder — the only tier that gives a near-zero-recovery-time answer to a Regional impairment — and also the most expensive, the most operationally demanding, and the one with the sharpest failure modes: the instant you accept writes in two Regions you sign up for replication lag, write conflicts, and a consistency model that is no longer “read your writes.” This article is the blueprint a senior engineer actually needs: the routing tier (Route 53 latency/failover/weighted/geoproximity records, health checks, and Application Recovery Controller routing controls), the entry-point decision (Global Accelerator anycast vs DNS), the data layer end to end (DynamoDB global tables with last-writer-wins, Aurora Global Database write-forwarding and managed failover/switchover, S3 Cross-Region Replication with Multi-Region Access Points, ElastiCache Global Datastore), the write-topology decision that governs everything, and the runbooks, game days and cost reality that separate a design that survives a Regional event from one that merely claims to.

By the end you will be able to place every component, state the RTO and RPO each choice actually delivers (not the marketing number), pick single-writer vs multi-writer for a workload and defend it, wire failover so it is deterministic instead of hopeful, and rehearse it with AWS Fault Injection Service before the real thing. Multi-region is not a checkbox; it is a set of trade-offs you make with open eyes, and the ones that bite hardest — conflict resolution, lag-as-RPO, static stability — are exactly the ones the happy-path tutorials skip.

What problem this solves

A single AWS Region is remarkably reliable, but “remarkably reliable” is not “never impaired.” Regional events happen — a control-plane degradation, a networking fault, a dependency (DNS, IAM, an internal service) that browns out a whole Region for hours — and when they do, everything you run there is affected at once, across every AZ, because a Regional impairment is not an AZ failure that Multi-AZ can route around. Multi-AZ buys survival of a data-centre loss; it buys nothing when the Region’s own control plane is sick. That is the gap multi-region closes, and the only thing that closes it.

The pain is concrete: a single-Region payments platform hit by a four-hour Regional impairment is down for four hours, because the failover target does not exist or exists only as cold infrastructure that takes hours to stand up and a database of unknown promotion time. Teams discover mid-incident that the “DR plan” was a wiki page describing a manual rebuild nobody has run since the last re-org, that the standby’s quotas cap it at a fraction of production load, and that “promote the read replica” targets a replica whose lag they have never measured. The outage is not the disaster; the unrehearsed, undersized, manually-triggered failover is.

Who hits this: anyone with a hard RTO/RPO contract (regulated payments, health, trading), anyone whose revenue-per-minute makes a multi-hour outage a board-level event, and anyone selling an availability SLA they cannot honour from one Region. It bites hardest on stateful workloads — a stateless web tier is trivial to run in two Regions; the entire difficulty lives in the data layer and the write topology. The fix is never “draw a second Region”; it is “make the second Region a peer continuously proven — by traffic, by rehearsal, and by pre-provisioned capacity — to take the load.”

What a single Region cannot survive, and what each protection level does and does not cover — this is why “we have Multi-AZ” is not an answer to a Regional-outage requirement:

Failure Single instance Multi-AZ (one Region) Multi-region active-active
Instance/host dies Down Survives (other AZ) Survives
AZ (data-centre) outage Down Survives Survives
Regional control-plane impairment Down Down (all AZs affected) Survives (other Region)
Regional network/DNS brownout Down Down Survives
Bad deploy poisons the Region Down Down Survives (deploy per-Region, shift away)
Data-corrupting bug replicated everywhere Down Down Not covered (needs backups/PITR)

Note the last row: multi-region protects against infrastructure loss, not logical corruption — a bad write replicates to both Regions, so you still need point-in-time recovery and backups underneath active-active.

To frame the field before the deep dive, here is the disaster-recovery ladder every team is somewhere on, and what each rung actually costs and delivers:

DR strategy What runs in the second Region Typical RTO Typical RPO Relative cost When it’s the right answer
Backup & restore Nothing live — snapshots/backups only Hours to a day Hours (last backup) ~5–10% of prod Dev/test, non-critical, tight budget
Pilot light Core data replicating; compute off/minimal Tens of minutes to hours Minutes (async replica) ~15–25% of prod Important but not tier-0; can tolerate a scale-up
Warm standby Full stack, scaled-down but running Minutes Seconds to minutes ~40–60% of prod Tier-1; needs fast recovery, cost-conscious
Active-active (multi-region) Full stack at production scale, taking traffic Seconds to low minutes Seconds (or ~0 with sync/switchover) ~100%+ of prod (often 2×+) Tier-0; Regional-outage survival is mandatory

The rest of this article is about the bottom row — but the row above it, warm standby, is the right answer far more often than teams admit, and one honest question decides between them: do you need to survive a Regional event with seconds of RTO, or is a few-minutes scale-and-promote acceptable? If a few minutes is fine, warm standby is dramatically cheaper and simpler. Active-active is for when it is not.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should be comfortable with single-Region AWS at a production level: VPCs across multiple Availability Zones, an Application Load Balancer in front of ECS Fargate or EC2, RDS/Aurora and DynamoDB basics, Route 53 hosted zones and record types, IAM roles, and infrastructure as code (Terraform or CloudFormation). You should understand the difference between an AZ (an isolated data-centre-scale failure domain inside a Region) and a Region (an independent geographic deployment with its own control plane), and why Multi-AZ inside one Region does not protect against a Regional event. Familiarity with RTO (recovery time objective — how long until service is restored) and RPO (recovery point objective — how much data loss is acceptable) is assumed throughout.

This article sits at the top of the resilience track. It builds directly on the single-Region foundation in The Classic AWS Three-Tier Web Application Architecture — the ALB → Auto Scaling → RDS stack you replicate per Region — and on the asynchronous, idempotent messaging patterns in EventBridge → SQS/SNS → Lambda: the event path and its decision points, because cross-region designs lean heavily on eventual consistency and at-least-once semantics. It is the AWS-native counterpart to the same problem on other clouds: the mechanics differ but the trade-offs are identical, so Azure Multi-Region Active-Active & Disaster Recovery and GCP Multi-Region Active-Active Architecture are worth reading side by side if you run more than one cloud.

A quick map of who owns what during a Regional incident, so you know which lever each team pulls:

Layer What lives here Who usually owns it Its role in a failover
DNS / routing Route 53 records, health checks, ARC Platform / SRE Steers users away from the impaired Region
Global entry Global Accelerator, CloudFront Platform / network Anycast fail-away; edge caching
Regional compute ALB, ECS/EKS/EC2, per-Region config App / platform Absorbs the shifted load (must be pre-scaled)
Relational data Aurora Global Database, RDS Data / DBA Promotion (switchover/failover) sets real RPO
NoSQL data DynamoDB global tables App / data Already multi-writer; conflicts resolved by LWW
Object/cache S3 CRR + MRAP, ElastiCache Global Datastore Platform / app Object failover routing; cache warm-up
Observability Cross-region CloudWatch, lag metrics SRE Tells you the lag/RPO before you decide to fail over

Core concepts

Six mental models make every later decision obvious.

A Region is a blast radius, and multi-region is the only thing that contains it. AWS gives you three nested failure domains: an instance/host, an Availability Zone (data-centre scale), and a Region (an independent deployment with its own control plane). Multi-AZ survives the middle one; only two Regions survive the outer one. Critically, the two must be operationally independent — if failing over to Region B requires a control-plane call in the impaired Region A (to scale, change DNS, or promote), then A’s impairment blocks your escape. This is static stability: Region B must keep serving and take more load using only resources and control planes that already exist and are healthy.

Active-active means writes in two places, and that changes everything about your data. A stateless compute tier is easy to duplicate; the hard part is state. The moment both Regions accept writes you inherit three problems that do not exist single-Region: replication lag, write conflicts (the same item written in both Regions at once), and a weaker consistency model (a read in Region B may not reflect a write in Region A yet). Every data-layer choice below is about how you handle these three, and the biggest — single-writer vs multi-writer — is whether you avoid conflicts or resolve them.

Replication is asynchronous, and lag is your live RPO. Cross-region replication is asynchronous by design, because synchronous replication across ~3,000+ km of fibre would add tens of milliseconds to every write and couple the two Regions’ availability. Asynchronous means there is always lag between a write committing at the source and appearing at the target — and that lag is your RPO at any instant. Lag of 800 ms means an unplanned failover loses up to 800 ms of writes; a spike to 45 s loses 45 s. That is why you alarm on lag: it is the single number that tells you what a failover will cost.

Planned and unplanned failover have different RPOs. A planned switchover (you initiate it, both Regions healthy) drains in-flight writes and achieves RPO 0 — Aurora’s managed switchover does exactly this. An unplanned failover (Region A impaired) cannot drain anything, so its RPO equals the replication lag at the moment of failure. Your numbers must state which kind they assume; quoting “RPO 0” without “on a planned switchover” is the most common way DR designs mislead.

DNS failover is slow and sticky; anycast failover is fast. Failing over at the DNS layer (Route 53 health checks flipping a record) is only as fast as detection + TTL + client resolver caching — realistically 60 s to several minutes, longer for misbehaving clients. Failing over at the anycast layer (Global Accelerator) reroutes at the backbone in seconds because the client’s IP never changes. That trade-off is why latency-critical workloads pay for Global Accelerator while cost-sensitive ones live with DNS.

Idempotency is not optional. Replication and failover both create duplicate and re-ordered delivery — a client retrying against Region B after A timed out submits the same operation twice. If every write is idempotent (keyed on a business identifier so a repeat is a no-op) these duplicates are harmless; if not, you double-charge the card and double-ship the order. Conditional writes on a business key plus a dedup TTL is the seatbelt that makes at-least-once, multi-writer semantics safe — required, not recommended.

The vocabulary in one table

Pin down every moving part before the deep sections; the glossary repeats these for lookup, but this is the mental model side by side:

Concept One-line definition Where it lives Why it matters to active-active
Region An independent AWS deployment with its own control plane Global The blast radius multi-region contains
Static stability Region B works without depending on Region A Design principle Failover must not call the sick Region
RTO / RPO Time-to-recover / data-loss-tolerated Contract The numbers every choice is measured against
Route 53 health check A probe that marks an endpoint healthy/unhealthy Global (us-east-1 metrics) Drives DNS failover
ARC routing control An on/off switch for traffic to a Region Application Recovery Controller Deterministic manual/gray failover
Global Accelerator Anycast static IPs on the AWS backbone Global (edge) Fast, DNS-independent failover
Global table A multi-writer, multi-Region DynamoDB table DynamoDB Multi-writer NoSQL; LWW conflicts
Last-writer-wins (LWW) Conflict rule: highest timestamp survives DynamoDB global tables Concurrent writes silently drop the loser
Aurora Global Database One primary + secondary-Region read replicas Aurora Relational cross-region; write-forwarding
Write-forwarding Secondary forwards writes to the primary Aurora Global DB Lets you write from a secondary Region
Managed switchover Planned, RPO-0 Aurora Region promotion Aurora Global DB The rehearsable, no-data-loss path
CRR / MRAP S3 Cross-Region Replication / Multi-Region Access Point S3 Object replication + single global endpoint
Replication lag Delay between source write and target visibility All async replication The live RPO; alarm on it

Why multi-region — and the DR tiers that lead up to it

Before committing to active-active, be honest about which DR tier your workload actually needs, because each rung up the ladder multiplies cost and operational burden. The four tiers are a menu, not a ladder you must climb, and most workloads should stop below the top. Backup & restore keeps nothing live in the second Region — snapshots only — provisioning from scratch on disaster (RTO hours-to-a-day). Pilot light keeps the data replicating but compute off/minimal, scaling up on disaster (RTO tens of minutes). Warm standby runs the full stack scaled-down (say 20%), scaling up and shifting on disaster (RTO minutes) — the sweet spot for most tier-1 systems and far cheaper than active-active. Active-active runs the full stack at production scale in both Regions, both serving; there is no “recovery,” only shifting the impaired Region’s traffic to the peer already handling its share (RTO seconds-to-low-minutes). Only the top tier gives a near-seamless answer to a Regional event, and only it is worth its cost when a multi-hour outage is existential.

The tiers compared on the axes that actually decide the choice:

Dimension Backup & restore Pilot light Warm standby Active-active
Second-Region compute None until disaster Minimal / off Running, scaled-down Full production scale
Second-Region data Snapshots only Live replica Live replica Live, bi-directional
RTO Hours–day 10s of min–hours Minutes Seconds–low minutes
RPO Backup interval Replica lag Replica lag Replica lag (~0 planned)
Traffic split in steady state 100/0 100/0 100/0 (or 99/1) 50/50 (or geo-weighted)
Failover action Provision + restore Scale + promote Scale + shift Shift traffic
Relative monthly cost ~5–10% ~15–25% ~40–60% ~100%+ (often 2×+)
Operational complexity Low Medium High Highest
Conflict handling needed? No No No (single-writer) Yes, if multi-writer

A worked example makes the numbers real. Say your contract is RTO 60 s, RPO 5 s. Backup & restore (RTO hours) fails outright; pilot light (RTO 20+ min) fails the RTO. Warm standby might meet RTO 60 s if scale-up and DNS failover are fast and pre-warmed, and meets RPO 5 s if replica lag stays under 5 s. Active-active meets both comfortably in steady state — the second Region is already serving, so “failover” is just re-weighting traffic, and RPO tracks the sub-second lag. The tighter the RTO/RPO, the higher up the ladder you are forced; RTO 60 s / RPO 5 s sits right at the boundary where warm standby stops being reliable and active-active becomes the safe choice.

The routing tier: Route 53 and Application Recovery Controller

Getting traffic to the right Region is the front half of the design. AWS gives you two mechanisms — Route 53 (DNS) and Global Accelerator (anycast) — and within Route 53, several routing policies that do very different jobs. Getting this layer wrong means users routed to a dead Region (no failover) or pinned to the far Region (bad latency); getting it right means each user lands on the closest healthy Region and moves off an impaired one deterministically.

Route 53 routing policies — pick the right one

A Route 53 record’s routing policy decides which answer a resolver gets. For active-active you almost always combine latency-based routing (send each user to the lowest-latency Region) with health checks (so an unhealthy Region drops from the answer set). The other policies solve adjacent problems: failover for strict active-passive, weighted for canary/blue-green and gradual shifting, geolocation/geoproximity for data-residency and bias, and multivalue for simple client-side spreading.

Routing policy What it does Best for Health-check aware? Gotcha
Latency-based Returns the Region with lowest measured latency to the resolver Active-active with geo-distributed users Yes “Latency” is AWS’s measurement, not the user’s real RTT
Geoproximity Routes by geographic distance, with an adjustable bias to expand/shrink a Region’s pull Deliberate geographic shaping Yes Requires Traffic Flow; more complex than latency
Geolocation Routes by the user’s continent/country/state Data residency, localized content Yes Needs a default record or some users get no answer
Failover Primary until unhealthy, then secondary Active-passive, strict Yes (required) Only two states; not for true active-active
Weighted Splits traffic by assigned weights (e.g. 90/10) Canary, blue-green, gradual shift Yes Weights are per-answer, not guaranteed per-user
Multivalue answer Returns up to 8 healthy records, client picks Simple spread without an LB Yes Not a load balancer; no latency awareness
Simple One static answer Single-Region, no failover No No failover at all — avoid for multi-region

The two records you actually deploy for latency-based active-active with failover:

# Region A (us-east-1) — latency record tied to a health check
aws route53 change-resource-record-sets --hosted-zone-id Z123EXAMPLE \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "api.shopfast.in",
        "Type": "A",
        "SetIdentifier": "us-east-1",
        "Region": "us-east-1",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "dualstack.alb-use1-123.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        },
        "HealthCheckId": "hc-use1-xxxx"
      }
    }]
  }'
# Terraform: two latency records, one per Region, each health-checked
resource "aws_route53_record" "api_use1" {
  zone_id        = var.zone_id
  name           = "api.shopfast.in"
  type           = "A"
  set_identifier = "us-east-1"

  latency_routing_policy { region = "us-east-1" }
  health_check_id = aws_route53_health_check.use1.id

  alias {
    name                   = aws_lb.use1.dns_name
    zone_id                = aws_lb.use1.zone_id
    evaluate_target_health = true
  }
}

resource "aws_route53_record" "api_usw2" {
  zone_id        = var.zone_id
  name           = "api.shopfast.in"
  type           = "A"
  set_identifier = "us-west-2"

  latency_routing_policy { region = "us-west-2" }
  health_check_id = aws_route53_health_check.usw2.id

  alias {
    name                   = aws_lb.usw2.dns_name
    zone_id                = aws_lb.usw2.zone_id
    evaluate_target_health = true
  }
}

The evaluate_target_health = true on the alias is the quiet hero: it means Route 53 will not return a Region whose ALB target group has no healthy targets, layering a second, faster health signal on top of the explicit health check.

Route 53 health checks — the failover trigger

A health check is a probe (from a fleet of global checkers) against an endpoint; when enough checkers report failure for a configured window, the check flips to unhealthy and Route 53 stops returning the associated record. A few properties govern how fast and reliably failover happens, and mis-tuning them is the difference between a 30-second failover and a five-minute — or flapping — one.

Health-check knob What it controls Default / options Effect on failover
Request interval Seconds between probes 30 s standard, 10 s fast Fast halves detection time (costs more)
Failure threshold Consecutive fails before unhealthy 3 (range 1–10) Lower = faster but flappier
Type HTTP/HTTPS/TCP/string-match/calculated/CloudWatch-alarm Calculated/CW-alarm enable rich logic
String matching Body must contain a token on/off (HTTP/HTTPS) Catches “200 OK but app broken”
Latency measurement Records probe latency on/off Diagnostics, not failover
SNI / port / path What exactly is probed configurable Probe a real health path, never /
Regions (checkers) Which checker Regions probe ≥3 by default More checkers = fewer false positives

Design rules that save incidents: probe a dedicated deep-health path (e.g. /healthz that verifies this Region can actually serve — DB reachable, dependencies up), not the homepage, so a broken app fails the check even when the web server answers. Use a fast (10 s) interval with a failure threshold of 3 for a ~30 s detection window on tier-0. And remember the quirk that costs hours in an incident: Route 53 health-check status and its CloudWatch metrics are published only in us-east-1. Build failover automation to read HealthCheckStatus from the Region it monitors and you build it wrong — the metric lives in N. Virginia regardless of which Region the check targets.

# A fast, string-matching health check against a deep-health endpoint
aws route53 create-health-check \
  --caller-reference "shopfast-usw2-$(date +%s)" \
  --health-check-config '{
    "Type": "HTTPS_STR_MATCH",
    "FullyQualifiedDomainName": "usw2.internal.shopfast.in",
    "ResourcePath": "/healthz",
    "SearchString": "SERVING",
    "Port": 443,
    "RequestInterval": 10,
    "FailureThreshold": 3,
    "MeasureLatency": true
  }'

Application Recovery Controller — deterministic failover you control

DNS health checks are reactive — they fail over when a Region looks broken to a probe. But many real Regional impairments are gray failures: the Region is degraded, not down, and health checks flap or stay green while error rates climb. For these, and for planned failovers and game days, you want a switch you throw deliberately, and that is Application Recovery Controller (ARC).

ARC has two parts. Readiness checks continuously audit whether your recovery Region is actually ready — do its Auto Scaling groups have the same capacity, are its quotas raised, do its resources match the primary — and flag drift before an incident, so you never discover mid-failover that Region B was quietly under-provisioned. Routing controls are highly-available on/off switches (backed by a cluster of five endpoints across five Regions, so the control plane you use to fail over is itself Regionally-independent) that you flip to shift traffic; each routing control is tied to a Route 53 health check, so flipping the control changes what DNS returns. A safety rule prevents you from turning both Regions off at once (or on, depending on your assertion). The whole point is static stability of the control plane: you can fail over even when the impaired Region’s own APIs are unreachable, because the ARC data plane lives elsewhere.

# Flip Region A OFF and Region B ON via ARC routing controls (data-plane endpoint)
aws route53-recovery-cluster update-routing-control-states \
  --region us-west-2 \
  --update-routing-control-state-entries '[
    {"RoutingControlArn":"arn:aws:route53-recovery-control::111122223333:controlpanel/abc/routingcontrol/use1","RoutingControlState":"Off"},
    {"RoutingControlArn":"arn:aws:route53-recovery-control::111122223333:controlpanel/abc/routingcontrol/usw2","RoutingControlState":"On"}
  ]'

When to reach for which routing mechanism:

Situation Mechanism Why
Steady-state geo-routing Route 53 latency + health checks Automatic, closest-healthy, no ops
Hard-down Region (clear failure) Route 53 health checks Detects and drops it automatically
Gray failure (degraded, flapping) ARC routing control You decide, deterministically, not a probe
Planned failover / game day ARC routing control Throw the switch, observe, throw it back
Fastest possible fail-away Global Accelerator Anycast, seconds, no DNS TTL
Verifying standby is ready ARC readiness checks Catches capacity/quota drift pre-incident

Global Accelerator vs Route 53 — anycast or DNS

Both put a stable name/address in front of two Regions, but they operate at different layers and fail over on different timescales, and the choice is one of the more consequential in the design.

Route 53 is DNS: the client resolves your name and gets an IP for a specific Region; failover means changing the DNS answer, which is bounded by TTL and client caching. Global Accelerator gives you two static anycast IPs advertised from AWS edge locations worldwide; the client always connects to the same IPs, traffic enters the AWS backbone at the nearest edge, and Global Accelerator routes it to the healthy Region over AWS’s private network. Failover is a backbone routing change, not a DNS change — it happens in seconds and the client never re-resolves anything, so even the worst DNS-caching client fails over instantly.

Dimension Route 53 (DNS) Global Accelerator (anycast)
Layer DNS (name → IP) Network (anycast IP on backbone)
Client-facing address Changes per Region Two static IPs, never change
Failover speed 60 s – several min (TTL + caching) Seconds (backbone reroute)
Failover trigger Health check flips DNS Endpoint health flips routing
Latency benefit Routes to low-latency Region Enters backbone at nearest edge, less jitter
Traffic control Weighted/latency/geo policies Traffic dials (per-endpoint %), no geo policies
Protocols Anything (it’s DNS) TCP/UDP (great for non-HTTP too)
Cost Cheap (queries + health checks) Fixed hourly + per-GB premium
Client IP preservation N/A Preserves client IP to the ALB
Best when Cost-sensitive, HTTP, geo policies needed Latency/failover-critical, static IPs required

Practically: use Route 53 alone for cost-sensitive HTTP workloads where a 1–2 minute DNS failover is acceptable and you want rich geo/weighted policies. Use Global Accelerator when you need sub-10-second failover, static IPs (for firewall allow-lists or IoT devices hard-coding an address), non-HTTP protocols, or lower latency/jitter via early backbone entry. Many tier-0 designs use both — Global Accelerator for the front door’s fast failover and static IPs, with Route 53 still owning the zone — because they solve overlapping but distinct problems.

# A Global Accelerator with two Regional ALB endpoints and a traffic dial
aws globalaccelerator create-accelerator --name shopfast-ga --ip-address-type IPV4
# ...then an endpoint group per Region, each pointing at that Region's ALB:
aws globalaccelerator create-endpoint-group \
  --listener-arn "$LISTENER_ARN" \
  --endpoint-group-region us-west-2 \
  --traffic-dial-percentage 100 \
  --endpoint-configurations "EndpointId=$ALB_USW2_ARN,Weight=128,ClientIPPreservationEnabled=true"

The traffic dial (0–100% per endpoint group) is Global Accelerator’s answer to weighted routing: dial us-east-1 to 0 and traffic drains to us-west-2 in seconds — a clean, DNS-independent manual failover that is ideal for game days.

The data layer: where multi-region gets hard

Everything above is the easy half. The data layer is where active-active earns its reputation, because it is where replication lag, write conflicts, and consistency live. AWS gives you a purpose-built multi-region option for each data shape — DynamoDB for key-value/document, Aurora for relational, S3 for objects, ElastiCache for cache — and each makes different promises. Choosing the wrong one, or misunderstanding its conflict model, is how teams lose data silently.

A map of the options and what each actually guarantees:

Data store Multi-region feature Write model Conflict resolution Consistency across Regions Typical replication lag
DynamoDB Global tables Multi-writer (all Regions) Last-writer-wins (timestamp) Eventual (or strong with MRSC) Typically < 1 s
DynamoDB (MRSC) Global tables, strong consistency Multi-writer, one write Region N/A (strongly consistent) Strong (up to 3 Regions) Higher; write-Region bound
Aurora Global Database Single-writer (+ write-forwarding) N/A (one logical writer) Read replicas lag primary Typically < 1 s (storage-level)
RDS (non-Aurora) Cross-Region read replica Single-writer N/A Async replica Seconds+
S3 CRR + Multi-Region Access Points Write to either bucket Version-based; LWW per key Eventual Seconds to minutes (RTC: 15-min SLA)
ElastiCache Global Datastore Single-writer (one primary Region) N/A Async; secondary read-only Sub-second typical
MemoryDB Multi-Region Single-writer per key space N/A Async Sub-second typical

DynamoDB global tables and the last-writer-wins conflict model

A DynamoDB global table is a set of identical tables in multiple Regions that DynamoDB keeps in sync automatically. It is multi-writer — every Region accepts writes to every item — which is exactly what makes it so convenient for active-active and exactly where the sharp edge is. When the same item is written in two Regions close enough in time that the writes cross in replication, DynamoDB resolves the conflict with last-writer-wins: the write with the highest timestamp survives, and the other is silently discarded. No error, no merge, no notification — the losing write simply vanishes.

This is fine for many workloads and catastrophic for a few. It is fine when items are naturally region-partitioned (each user/tenant writes predominantly in one Region) or when writes are commutative/idempotent (setting a status, upserting a profile). It is dangerous for shared mutable counters or balances — two Regions incrementing the same item concurrently loses an increment, because LWW overwrites, it does not add. The mitigations are architectural: region-pin writers so an item is only written from one Region (effective single-writer per item), model writes as immutable append events (no conflicts if you never overwrite), or use multi-Region strong consistency (MRSC) — strongly consistent across up to three Regions, at the cost of routing writes through a designated Region and higher latency.

Global-tables property Behaviour Implication
Replication scope All items, all attributes, all Regions Full copy everywhere; cost scales with Regions
Write acceptance Every Region accepts every write True multi-writer convenience
Conflict rule (eventual) Last-writer-wins by timestamp Concurrent same-item writes lose the loser
Conflict rule (MRSC) Strongly consistent, no lost writes Higher latency; write-Region-bound; ≤3 Regions
Replication mechanism Async via DynamoDB Streams internally Requires Streams; near-real-time
Typical lag Usually well under a second Small but non-zero RPO
Lag metric ReplicationLatency (per source→dest) Alarm on it — it’s your RPO
Reserved attribute Adds aws:rep:* metadata Don’t collide with these attribute names
Prerequisite Streams enabled; matching table config Can’t retrofit blindly onto a live table without care
# Turn an existing table into a global table by adding a replica Region
aws dynamodb update-table --table-name Orders \
  --replica-updates '[{"Create": {"RegionName": "us-west-2"}}]'

# Watch replication latency — this number IS your RPO
aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB \
  --metric-name ReplicationLatency \
  --dimensions Name=TableName,Value=Orders Name=ReceivingRegion,Value=us-west-2 \
  --start-time "$(date -u -v-1H +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
  --period 60 --statistics Average Maximum
# Terraform: a global table (v2) with two replicas and Streams on
resource "aws_dynamodb_table" "orders" {
  name             = "Orders"
  billing_mode     = "PAY_PER_REQUEST"
  hash_key         = "orderId"
  stream_enabled   = true
  stream_view_type = "NEW_AND_OLD_IMAGES"

  attribute {
    name = "orderId"
    type = "S"
  }
  replica { region_name = "us-west-2" }
}

The consistency guarantee a read gives you differs by store and by where you read — this is the table to reason from when a user complains “I saved it and it’s gone”:

Read scenario DynamoDB global table Aurora Global DB S3 CRR
Read own write, same Region Strong (if ConsistentRead) Strong (from primary) Read-after-write on the source
Read own write, other Region Eventual (lag window) Replica lag window Eventual (lag window)
Read after failover May miss un-replicated writes Missing up to lag (unplanned) Missing pending-replication objects
Concurrent cross-Region writes LWW resolves silently N/A (single writer) LWW per key by version
Strong across Regions Only with MRSC (≤3 Regions) Reads always lag primary Not applicable

Aurora Global Database — relational across Regions

Relational data cannot be casually multi-writer (foreign keys, transactions, and unique constraints make LWW meaningless), so Aurora Global Database takes a different shape: one primary Region that accepts writes, and secondary Regions that hold read-only replicas kept in sync by storage-level replication — Aurora replicates at the storage layer, not by shipping SQL, so lag is typically under a second even across continents and adds no load to the primary’s compute.

To let application servers in a secondary Region issue writes without repointing to the primary’s endpoint, Aurora offers write-forwarding: a secondary-Region writer endpoint transparently forwards write statements to the primary and applies them, so your app can “write locally” while the actual write still funnels to the single primary (preserving relational integrity). You choose a read-consistency level for forwarded writes (eventual, session, or global) trading latency for how much of your own/others’ writes a subsequent read sees.

The failover story has two distinct paths, and conflating them is the classic RPO mistake:

Aurora Global DB aspect Detail Why it matters
Write model Single primary Region Preserves relational integrity, no LWW
Secondary Regions Up to 5, read-only replicas Local low-latency reads globally
Replication Storage-level, async Sub-second lag, no primary CPU cost
Write-forwarding Secondary forwards writes to primary Write “locally” without repointing endpoint
Read consistency (forwarding) eventual / session / global Latency vs “see your writes” trade-off
Managed switchover Planned, drains, RPO 0 The rehearsable, no-loss path
Unplanned failover Promote secondary; RPO = lag Real Regional loss; some data may be lost
Typical RTO Low single-digit minutes Promotion + endpoint/DNS repoint
Lag metric AuroraGlobalDBReplicationLag (ms) Your live relational RPO
Endpoints Cluster/reader per Region + global writer Apps target Region-local endpoints
# Planned, zero-data-loss switchover to the secondary Region
aws rds switchover-global-cluster \
  --global-cluster-identifier shopfast-global \
  --target-db-cluster-identifier arn:aws:rds:us-west-2:111122223333:cluster:shopfast-usw2

# Unplanned failover (real Regional loss) — accepts data loss up to current lag
aws rds failover-global-cluster \
  --global-cluster-identifier shopfast-global \
  --target-db-cluster-identifier arn:aws:rds:us-west-2:111122223333:cluster:shopfast-usw2 \
  --allow-data-loss

The two flags tell the whole story: switchover-global-cluster has no --allow-data-loss because there is none; failover-global-cluster --allow-data-loss is the “the Region is gone, take what we have” button you press only in a genuine impairment.

S3 Cross-Region Replication and Multi-Region Access Points

Objects replicate with S3 Cross-Region Replication (CRR): a replication rule copies newly written objects from a source bucket to a destination bucket in another Region, asynchronously. Two properties trip teams up. First, CRR replicates new objects going forward — it does not back-fill objects that existed before you enabled it (you need S3 Batch Replication for that), and by default it does not replicate delete markers or existing-object metadata changes unless you configure it to. Second, replication is eventually consistent with a variable lag (seconds to minutes); if you need a bound, S3 Replication Time Control (RTC) provides a 15-minute SLA and replication metrics.

To give applications a single global endpoint that fails over between buckets, use a Multi-Region Access Point (MRAP): one hostname routes requests to the nearest/healthy bucket, with failover controls you flip (active/passive per Region, 0/100) to shift object traffic — the S3 analogue of a Route 53 failover. Writes through a MRAP go to one bucket and replicate out via CRR.

S3 multi-region property Behaviour Gotcha / mitigation
CRR scope New objects, source → dest Existing objects need Batch Replication
Delete markers Not replicated unless enabled Enable if you need deletes to propagate
Lifecycle actions Never replicated Set lifecycle independently in each bucket
Consistency Eventual, variable lag Use RTC for a 15-min SLA + metrics
Bi-directional Configure CRR both ways Watch for replication loops (uses replica markers)
MRAP Single global endpoint Routes to nearest/healthy; failover controls
MRAP failover Manual/active-passive 0/100 The S3 “flip the Region” switch
Metric OperationsPendingReplication, BytesPending... Alarm — pending ops = un-replicated data at risk
SigV4A MRAP requires SigV4A signing SDK/tooling must support it
# A replication rule with RTC (15-min SLA) and metrics
aws s3api put-bucket-replication --bucket shopfast-assets-use1 \
  --replication-configuration '{
    "Role": "arn:aws:iam::111122223333:role/s3-crr-role",
    "Rules": [{
      "ID": "to-usw2", "Status": "Enabled", "Priority": 1,
      "Filter": {}, "DeleteMarkerReplication": {"Status": "Enabled"},
      "Destination": {
        "Bucket": "arn:aws:s3:::shopfast-assets-usw2",
        "ReplicationTime": {"Status": "Enabled", "Time": {"Minutes": 15}},
        "Metrics": {"Status": "Enabled", "EventThreshold": {"Minutes": 15}}
      }
    }]
  }'

ElastiCache Global Datastore

Cache is not a source of truth, but a cold cache in the failover Region can turn a clean failover into a latency cliff and a thundering herd on the database. ElastiCache Global Datastore (Redis/Valkey) keeps a single primary Region replicating to read-only secondaries with sub-second lag; on failover you promote a secondary to primary. The rule is simple: treat the cache as warm-but-not-authoritative — a secondary that has been replicating is warm, so promoting it avoids the cold-start herd, but never rely on the cache for correctness across the failover, only for performance.

ElastiCache Global Datastore aspect Detail
Topology One primary Region, up to 2 secondaries
Secondary role Read-only until promoted
Replication Async, typically sub-second
Failover Promote a secondary to primary
Value in failover Warm cache avoids thundering-herd on DB
Caveat Not authoritative; correctness lives in the DB

The write-topology decision — the choice that governs everything

Above all the per-service detail sits one decision that shapes the entire architecture: where do writes happen? Get this right and the rest follows; get it wrong and you fight conflicts and lag forever. There are three practical topologies.

Single global writer (active-active reads, single-Region writes). Both Regions serve reads locally, but all writes go to one Region (via write-forwarding or by routing writes to the primary’s endpoint). One logical writer means LWW never fires and relational integrity holds — at the cost of cross-region write latency for far users and a failover that must promote a new writer. The default, correct choice for relational data and anything with strong-consistency or unique-constraint needs.

Multi-writer with region-pinning. Both Regions accept writes, but you partition the keyspace so each item is only ever written from one Region — pin a tenant/user/shard to a “home” Region and route its writes there. This gives local write latency and no conflicts (effective single-writer per item), the sweet spot for partitionable workloads (per-tenant SaaS, per-user data) and a natural fit for DynamoDB global tables. The cost is routing complexity — a stable, fast lookup of each key’s home Region — and a plan to temporarily re-home a down Region’s keys.

Full multi-writer (accept and resolve conflicts). Every Region writes every item and you resolve the conflicts — LWW for DynamoDB, or app-level merge (CRDTs, append-only logs, reconciliation). Lowest write latency and simplest routing, but it demands a data model that tolerates conflicts: immutable events, commutative operations, or explicit merge. Right for conflict-free/tolerant data (event streams, analytics ingestion, idempotent status updates), wrong for anything where a silently-lost write is unacceptable (balances, inventory decrements, bookings).

Write topology Write latency Conflict handling Failover impact Best data shape
Single global writer High for far users None (one writer) Must promote a writer (adds RTO) Relational, strong-consistency
Multi-writer, region-pinned Low (local) None (partitioned) Re-home the down Region’s keys Partitionable (per-tenant/user)
Full multi-writer Low (local) LWW or app merge Trivial (already everywhere) Conflict-free/tolerant, event data

Whichever you pick, idempotency is mandatory, because failover and retries produce duplicate writes. The pattern is the same everywhere: derive a deterministic idempotency key from the business operation (e.g. order#<id>#placed), and make the write conditional on that key not already existing, with a TTL to bound storage.

# Idempotent write: a duplicate delivery (from retry or failover) is a no-op
def place_order(table, order_id, payload):
    try:
        table.put_item(
            Item={"pk": f"ORDER#{order_id}", "sk": "PLACED", **payload,
                  "ttl": int(time.time()) + 30 * 86400},
            ConditionExpression="attribute_not_exists(pk)")
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            return  # already placed — safe no-op, not an error
        raise

A decision table for picking a topology from the workload:

If the data is… And you need… Pick Because
Relational with constraints Correctness over write latency Single global writer (Aurora) LWW would corrupt relational invariants
Per-tenant / per-user Local writes + no conflicts Multi-writer, region-pinned Partitioning eliminates conflicts
Balances / inventory No lost writes, ever Single writer or MRSC / app locks LWW silently drops an increment
Event / append-only streams Lowest latency, high throughput Full multi-writer Immutable appends never conflict
Idempotent status upserts Simplicity + local writes Full multi-writer (LWW) Commutative writes tolerate LWW
Bookings / reservations Strong consistency Single writer or MRSC Double-booking is unacceptable

Stateless compute per Region, and static stability

With routing and data decided, the compute tier is comparatively simple if you hold one line: each Region’s compute must be stateless and self-sufficient. No session state on the instance (push it to the global data layer or a signed token), no dependency on the other Region to serve a request, and — the part teams skip — enough capacity, already provisioned, to absorb the other Region’s traffic when it fails over.

That last point is static stability applied to compute, and the most expensive truth in active-active. Run each Region at its own steady-state load and, when one fails, the survivor must instantly serve 2× its normal traffic. Relying on autoscaling to catch up is a trap: scaling takes minutes (launch, warm, register, health-check), the surge is instant, and a Regional impairment may be the very thing degrading the scaling control plane you depend on. The robust pattern is pre-provisioned headroom — run each Region able to take the full failover load without scaling (or with a large warm pool) — so failover is a routing change, not a race against autoscaler latency. This is why active-active often costs more than 2×: each side carries failover headroom.

Compute concern Anti-pattern Static-stability pattern
Session state Sticky sessions on the instance Stateless + global data/token store
Failover capacity Rely on autoscaling to 2× Pre-provisioned headroom / warm pools
Config/secrets Fetched from the other Region Region-local (SSM/Secrets Manager per Region)
Service discovery Cross-Region calls in the hot path Region-local endpoints only
Quotas Default limits in the standby Region Quotas raised to full-load in both Regions
Deploys Deploy to one Region only Identical IaC, deploy to both, drift-checked
Control-plane dependency Failover calls the impaired Region Data-plane-only failover (ARC, GA dial)

The quota point deserves emphasis because it is the silent killer: service quotas (running tasks, ENIs, Lambda concurrency, NAT gateway bandwidth) are per-Region, and a standby Region provisioned “small” often has default quotas that cap it far below full production load. When it must take 2× traffic, it hits a quota wall and throttles — during the incident, when you can least afford it. ARC readiness checks exist precisely to catch this drift before the incident; use them, and raise the standby’s quotas to match production, not to match its steady-state.

Failover runbooks, game days, and cross-region observability

A multi-region architecture is only as good as the failover you can execute under pressure at 3 a.m., and the only way to know it works is to have run it. Three disciplines make the difference: a runbook that is deterministic and scripted (not a wiki page of prose), game days that rehearse it at full load with AWS Fault Injection Service (FIS), and cross-region observability that shows you both Regions and the lag between them so you can decide before you pull the trigger.

The failover runbook — deterministic, not hopeful

An unplanned Regional-failover runbook is a short, ordered sequence where every step is a single scripted action with a verification, and — critically — every step uses a data-plane control that does not depend on the impaired Region. The canonical sequence:

# Step Action (data-plane) Verify Notes
1 Confirm it’s Regional, not app Check health across services in the Region Multiple services failing, not one Don’t fail over for a single-service blip
2 Read the current replication lag CloudWatch lag metrics (from us-east-1) Lag value = the RPO you’ll incur This is the data-loss decision point
3 Decide: accept the lag as RPO Human/automated go/no-go Lag ≤ RPO budget → proceed State the accepted loss explicitly
4 Shift traffic away ARC routing control flip / GA dial to 0 New requests hit the healthy Region Seconds; no DNS wait if GA/ARC
5 Promote the writer failover-global-cluster --allow-data-loss New primary accepts writes RPO = lag from step 2
6 Promote cache (if used) ElastiCache Global Datastore promote Secondary now primary, writable Warm cache avoids DB herd
7 Flip S3 object routing MRAP failover control (0/100) Writes land in the surviving bucket Objects mid-replication may be pending
8 Verify end-to-end Synthetic checkout on survivor Success rate recovers The only proof that matters
9 Communicate + monitor Status page, watch lag/errors Steady state on one Region Plan the fail-back separately

Fail-back (returning to the recovered Region) is a separate, planned runbook run when the Region is healthy — and it should use the managed switchover (RPO 0) path, never the data-loss failover, because there is no emergency justifying data loss on the way back.

Game days with AWS Fault Injection Service

FIS runs controlled failure experiments so you rehearse the runbook against realistic faults instead of hoping. An experiment template defines the fault, the targets, and a stop condition (a CloudWatch alarm that aborts the experiment if real damage exceeds a threshold). The experiments worth running quarterly:

Experiment What FIS injects What it proves / finds
Regional evacuation Blackhole traffic + ARC flip to one Region Survivor capacity (the cold-standby trap), full RTO
AZ failure in a Region Stop instances / disrupt one AZ Multi-AZ within the Region still holds
Aurora failover Trigger a DB cluster failover Promotion time, app reconnect behaviour
Dependency latency Inject latency on a downstream Timeout/retry/idempotency behave under stress
Throttle injection API error/throttle on a service Backoff and quota headroom hold up
Replication-lag spike Load-generate writes, watch lag Lag alarms fire; RPO decision is exercised

The rule the ShopFast team wrote holds universally: a failover you have not rehearsed at full load does not exist. Steady-state health tells you nothing about whether the survivor can take 2× — only the game day does.

Cross-region observability — see both Regions and the lag

You cannot operate what you cannot see, and multi-region doubles the surface. The observability that matters is not “is each Region up” (necessary but insufficient) — it is a single pane that shows both Regions side by side and the replication lag between them, because lag is the number the failover decision hinges on. The signals to wire, and where each lives:

Signal Metric / source Region it’s published in Alarm at
Aurora replication lag AuroraGlobalDBReplicationLag (ms) Each Region’s cluster RPO/2
DynamoDB replication lag ReplicationLatency (per dest) Source Region RPO/2
S3 pending replication OperationsPendingReplication Source bucket Region > 0 sustained
Route 53 health status HealthCheckStatus us-east-1 only Any unhealthy
Per-Region error rate ALB/target 5xx, app metrics Each Region > SLO
Per-Region saturation CPU/concurrency/task count Each Region Approaching failover capacity
Global Accelerator health Endpoint health, flow logs Global Endpoint unhealthy

Two platform quirks to encode in the tooling: Route 53 health-check metrics live only in us-east-1 (build the dashboard to read them there, always), and cross-region metric math requires either a monitoring account with cross-account/cross-Region observability enabled or shipping metrics to one place — a per-Region dashboard nobody looks at during an incident is worse than useless. Put the two Regions and the lag on one screen, or the runbook’s “read the lag” step becomes a scramble.

Architecture at a glance

Read the system left to right. Users worldwide resolve api.shopfast.in and are steered by the global entry tier — Route 53 latency-based records backed by fast health checks, optionally fronted by Global Accelerator’s anycast IPs — to the closest healthy Region. Each Regional stack (us-east-1 and us-west-2) is a complete, self-sufficient copy: an ALB with WAF terminating TLS on a regional ACM cert, a stateless ECS Fargate service spread across three AZs, and a Region-local database endpoint. Neither Region needs the other to serve a request; the second stack is not a cold standby but a pre-scaled peer carrying its own traffic plus failover headroom. Underneath both, the global data tier replicates continuously and asynchronously: DynamoDB global tables accept writes in both Regions (resolving concurrent same-item writes by last-writer-wins), Aurora Global Database keeps a sub-second read replica in the secondary Region for a single-writer relational core, and S3 Cross-Region Replication with a Multi-Region Access Point mirrors objects with a 15-minute RTC SLA. CloudWatch watches the replication lag on every stream, because that lag is your live RPO and the number that decides what a failover costs.

The six numbered badges mark the decisions that turn this from a drawing into a design that survives a Regional event: the DNS failover window (detection plus TTL), Aurora promotion (switchover for RPO 0 vs failover-with-data-loss), the cold-standby capacity trap, DynamoDB’s last-writer-wins conflict semantics, S3 replication gaps (new objects only; deletes and lifecycle need care), and lag-as-RPO alarming. Read each badge as a place where the happy path and the real path diverge.

AWS multi-region active-active architecture: users routed by Route 53 latency records and health checks (with optional Global Accelerator anycast) to the closest healthy Region; two mirrored Regional stacks (us-east-1 and us-west-2) each running an ALB with WAF, stateless ECS Fargate across three AZs, and a Region-local Aurora endpoint; a global data layer of DynamoDB global tables with last-writer-wins, Aurora Global Database with sub-second storage replication, and S3 Cross-Region Replication with a Multi-Region Access Point; CloudWatch tracking replication lag as live RPO; six numbered badges marking the DNS failover window, Aurora promotion, cold-standby capacity trap, LWW write conflicts, S3 replica gaps, and lag-as-RPO alarms

Real-world scenario

ShopFast, a fictional Indian D2C commerce platform, ran entirely in ap-south-1 (Mumbai) and had grown to ₹9 crore in monthly GMV. Their availability SLA to marketplace partners promised 99.95%, but a single Region cannot honour that against a Regional event, and their board — after a competitor lost a full day of sales to a Regional impairment — mandated active-active with an RTO of 90 seconds and RPO of 10 seconds for the checkout path.

The team started, correctly, by classifying data rather than lifting-and-shifting. Carts, sessions, and product-view events went to DynamoDB global tables in ap-south-1 and ap-southeast-1 (Singapore) — naturally per-user-partitioned, so they region-pinned each user’s cart writes to their home Region and never hit an LWW conflict in practice. Orders, payments, and inventory — relational, constraint-heavy, and absolutely intolerant of a silently-lost write — went to Aurora Global Database with ap-south-1 as the single writer and Singapore as a sub-second read replica, using write-forwarding so Singapore app servers could accept order writes that funnelled to the Mumbai primary. Product images and invoices went to S3 with CRR + RTC and a MRAP front door. The write topology was deliberately mixed: multi-writer region-pinned for carts, single-global-writer for money.

The first game day was humbling. Using AWS Fault Injection Service, they simulated an ap-south-1 impairment by blackholing traffic and flipping the ARC routing control to shift everything to Singapore. Three things broke that the diagram had hidden. First, Singapore’s ECS service was scaled to its steady-state (30% of total), and when it took 100% of traffic it saturated and throttled while autoscaling crawled — a textbook cold-standby capacity trap. Second, Singapore’s Lambda concurrency quota was the default, far below production need, so background workers throttled during the surge. Third, checkout latency spiked because with Mumbai gone, write-forwarding had nowhere to forward until Aurora was switched over — and the runbook’s step for that was a manual console click nobody had timed.

The fixes were static-stability fixes. They pre-scaled Singapore to carry full failover load (accepting the cost — this is why active-active exceeds 2×), raised every per-Region quota to production levels in both Regions and wired ARC readiness checks to alarm on drift, and rewrote the runbook so an unplanned Mumbai loss triggers failover-global-cluster --allow-data-loss (accepting RPO = current lag, which their AuroraGlobalDBReplicationLag alarm confirmed sat under 400 ms) with the ARC routing-control flip as a single scripted action. The next game day hit RTO 74 seconds end to end (ARC flip + Aurora promotion + Global Accelerator dial to 0), with measured RPO under 1 second because lag was healthy at the moment of failover. The lesson the team wrote at the top of their runbook: the failover you have never rehearsed at full load does not exist — the design on paper met the SLA; only the game day made it real.

Advantages and disadvantages

Active-active is powerful and expensive, and the trade-off is stark. The explicit two-column view:

Advantages Disadvantages
Survives a full Regional impairment (the only tier that does) Highest cost — often 2×+ a single Region
Near-zero RTO in steady state (both Regions already serving) Highest operational complexity
Low latency globally (users hit the closest Region) Data layer inherits lag, conflicts, weaker consistency
RPO ~0 on planned switchover Unplanned failover RPO = replication lag (non-zero)
No “recovery” scramble — failover is a routing change Requires pre-provisioned failover headroom (extra cost)
Rehearsable with game days / FIS Multi-writer risks silent data loss (LWW) if misused
Both Regions continuously prove they work (live traffic) Cross-region data-transfer bill is real and ongoing
Blue-green/canary per Region for safer deploys Quota and drift management doubles

When each side matters: the advantages are decisive only when a multi-hour Regional outage is existential — regulated payments, a marketplace SLA with penalties, a trading system. For most workloads the disadvantages dominate and warm standby is the wiser choice, delivering minutes-RTO at a fraction of the cost and complexity without ever accepting multi-writer conflict risk. The honest test: if you cannot articulate a specific, quantified business loss from a 30-minute Regional outage that exceeds the annual cost of the second full Region plus the engineering to operate it, you do not need active-active — you need warm standby and a good runbook.

Hands-on lab

This lab stands up the routing and NoSQL data half of an active-active design in two Regions using a DynamoDB global table and Route 53 latency records with health checks — enough to see cross-region replication and DNS failover work, staying within or near the free tier. (A full Aurora Global Database is out of free-tier scope; the CLI for it is in the sections above.)

1. Create a table in Region A and make it global.

aws dynamodb create-table --region us-east-1 --table-name lab-orders \
  --attribute-definitions AttributeName=orderId,AttributeType=S \
  --key-schema AttributeName=orderId,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES

aws dynamodb update-table --region us-east-1 --table-name lab-orders \
  --replica-updates '[{"Create": {"RegionName": "us-west-2"}}]'

2. Write in Region A, read in Region B — watch it replicate.

aws dynamodb put-item --region us-east-1 --table-name lab-orders \
  --item '{"orderId": {"S": "A-1001"}, "amount": {"N": "4999"}}'

# Within ~1 second it appears in us-west-2:
aws dynamodb get-item --region us-west-2 --table-name lab-orders \
  --key '{"orderId": {"S": "A-1001"}}'
# Expected: the item with amount 4999

3. Write in Region B, read in Region A — confirm it’s truly multi-writer.

aws dynamodb put-item --region us-west-2 --table-name lab-orders \
  --item '{"orderId": {"S": "B-2002"}, "amount": {"N": "1299"}}'
aws dynamodb get-item --region us-east-1 --table-name lab-orders \
  --key '{"orderId": {"S": "B-2002"}}'
# Expected: the item written in us-west-2, now visible in us-east-1

4. Observe replication latency (your RPO).

aws cloudwatch get-metric-statistics --region us-east-1 --namespace AWS/DynamoDB \
  --metric-name ReplicationLatency \
  --dimensions Name=TableName,Value=lab-orders Name=ReceivingRegion,Value=us-west-2 \
  --start-time "$(date -u -v-30M +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
  --period 60 --statistics Average Maximum

5. (Optional) See LWW in action. Write the same orderId in both Regions within the same second (script both put-item calls back to back); after replication settles, get-item from both Regions returns the same value — the later-timestamped write. The other write is gone, silently. This is the conflict model you must design around.

6. Create health checks and latency records (requires a hosted zone and two public endpoints — e.g. two ALBs or two API Gateway stages). Follow the Route 53 CLI in the routing section, one latency record per Region each bound to a health check, then disable one endpoint and watch dig api.yourzone return only the healthy Region after the failure threshold.

7. Teardown — delete replicas first, then the table.

aws dynamodb update-table --region us-east-1 --table-name lab-orders \
  --replica-updates '[{"Delete": {"RegionName": "us-west-2"}}]'
# wait for the replica to be removed, then:
aws dynamodb delete-table --region us-east-1 --table-name lab-orders
# Also delete any Route 53 health checks and records you created.

Common mistakes & troubleshooting

Active-active fails in characteristic ways, almost all of them at the seams — the data layer, the failover trigger, and capacity. Each below is a real failure mode with how to confirm it and the fix.

# Symptom Root cause How to confirm Fix
1 Failover takes 5+ minutes, users still hit the dead Region DNS TTL too high + client resolver caching Check record TTL; dig shows old answer post-failover Low TTL (30–60 s); use Global Accelerator or ARC for seconds
2 Writes silently disappear under concurrent load DynamoDB LWW dropping the losing write Conflict-canary items; compare writes vs stored Region-pin writers, or MRSC, or append-only model
3 Standby Region throttles the moment it takes traffic Default per-Region quotas + no pre-scaling ARC readiness check flags drift; throttle errors spike Raise quotas to full load; pre-provision headroom
4 Unplanned failover loses more data than expected Replication lag was high at failure (= RPO) AuroraGlobalDBReplicationLag / ReplicationLatency history Alarm on lag at RPO/2; decide before failing over
5 Old objects missing in the failover Region CRR replicates new objects only, no back-fill Object count mismatch between buckets Run S3 Batch Replication to back-fill
6 Deletes don’t propagate; deleted objects reappear Delete-marker replication not enabled Object present in dest after source delete Enable DeleteMarkerReplication in the rule
7 Health check green but Region is actually broken Probing / not a deep-health path App down yet / returns 200 Probe /healthz that checks DB + deps; string-match
8 Failover automation reads no health-check metric Route 53 health metrics only exist in us-east-1 HealthCheckStatus empty in the target Region Read the metric from us-east-1
9 Cross-region write latency tanks checkout Write-forwarding funnelling far to the primary Latency to primary Region in traces Region-pin writes, or accept and cache; pick topology
10 Both Regions accidentally routed off (total outage) Missing ARC safety rule Both routing controls Off simultaneously Add an ARC safety rule preventing both-off
11 Replication loops / doubled objects (bi-dir S3) Bi-directional CRR re-replicating replicas Objects bouncing between buckets Rely on replica-marker exclusion; scope rules
12 Bill explodes after go-live Cross-region data transfer + doubled compute Cost Explorer: DataTransfer-Regional + duplicated resources Model it (below); minimize chatty cross-region calls

Deeper on the three that hurt most

Silent write loss (row 2) is the scariest because there is no error. The confirmation technique is a conflict canary: a background job that writes a known, monotonically-increasing value to the same item from both Regions and periodically checks whether any writes were lost; a gap proves LWW is dropping writes. The moment you see it, the data model — not a config — is wrong for multi-writer, and the fix is architectural (region-pin, MRSC, or append-only).

Lag-as-RPO surprise (row 4) happens when a team quotes “RPO 0” (true for planned switchover) and then suffers an unplanned failover during a lag spike, losing seconds of writes they thought were safe. The fix is cultural as much as technical: alarm on replication lag at RPO/2 on every stream (AuroraGlobalDBReplicationLag, DynamoDB ReplicationLatency, S3 OperationsPendingReplication), and make the runbook state explicitly that an unplanned failover loses up-to-current-lag — so the on-call engineer decides to accept that loss with eyes open, rather than discovering it after.

Capacity trap (row 3) is the most common game-day failure and the reason game days exist. A standby sized for its own steady-state cannot absorb the other Region’s load without either pre-provisioned headroom or fast-enough scaling — and autoscaling is rarely fast enough for an instant 2× surge, and may itself be impaired. Confirm with a full-load game day (FIS); fix by pre-scaling and raising quotas. There is no shortcut: the only proof a Region can take failover load is watching it do so.

Best practices

Security notes

Cost & sizing

The uncomfortable headline: active-active typically costs more than 2× a single Region, not exactly 2×, because each Region carries failover headroom on top of its own load. Where the money actually goes:

A rough monthly picture for a mid-size API doing meaningful write volume, active-active across two Regions (illustrative, INR):

Cost driver What you pay for Rough INR / month Notes
Compute (2 Regions, headroom) 2× ECS/EC2 at ~2× peak capacity ~₹2,00,000–4,00,000 Largest item; warm standby is far less
DynamoDB global table Replicated writes + storage ×2 ~₹40,000–1,20,000 Replicated write units cost extra
Aurora Global Database Primary + secondary clusters ~₹80,000–2,00,000 Secondary instances + storage
Cross-region data transfer Replication + app cross-calls ~₹20,000–1,00,000+ Scales with write volume — model it
S3 CRR + storage Destination copy + RTC ~₹10,000–50,000 RTC adds a small per-GB premium
Global Accelerator (if used) Hourly + per-GB ~₹3,000–15,000 Skip if Route 53 DNS failover suffices
Route 53 + health checks Queries + fast checks ~₹1,000–5,000 Cheap; fast checks cost a little more
CloudWatch (2 Regions) Metrics, logs, dashboards ~₹10,000–40,000 Doubles with the second Region

Right-sizing levers: run warm standby instead of active-active if minutes-RTO is acceptable (the single biggest saving); region-pin reads so cross-region traffic is only replication, not application chatter; use on-demand/PAY_PER_REQUEST for spiky DynamoDB rather than over-provisioned capacity; and drop Global Accelerator if DNS failover meets your RTO. The free-tier reality: you can learn multi-region cheaply (DynamoDB global tables and Route 53 fit a lab budget), but you cannot run production active-active cheaply — the doubled stack is the point, and it is the cost.

Interview & exam questions

1. What is the difference between RTO and RPO, and why does the kind of failover change the RPO? RTO is how long until service is restored; RPO is how much data loss is acceptable. A planned switchover drains in-flight replication and achieves RPO 0; an unplanned failover cannot drain and so its RPO equals the replication lag at the moment of failure. Quoting “RPO 0” without saying “on a planned switchover” is misleading — the unplanned path always loses up-to-current-lag.

2. Why doesn’t Multi-AZ protect against a Regional event? Multi-AZ spreads across data-centre-scale failure domains within one Region, surviving an AZ loss. A Regional impairment affects the Region’s own control plane and shared services across all AZs at once, so there is no healthy AZ to route to. Only running in a second Region — with an independent control plane — survives it.

3. Explain DynamoDB global tables’ last-writer-wins model and when it’s dangerous. Global tables are multi-writer; concurrent writes to the same item in two Regions are resolved by keeping the highest-timestamp write and silently discarding the other. It’s safe for region-partitioned or commutative/idempotent writes but dangerous for shared mutable counters/balances, where a lost increment is silent data corruption. Mitigate with region-pinning, an append-only model, or multi-Region strong consistency (MRSC).

4. What is write-forwarding in Aurora Global Database, and what does it trade off? Write-forwarding lets application servers in a secondary Region issue writes that Aurora transparently forwards to the single primary, so you can “write locally” without repointing to the primary’s endpoint — preserving relational integrity with one logical writer. The trade-off is added latency (writes still funnel to the primary) and a read-consistency choice (eventual/session/global) for whether a subsequent read sees the forwarded write.

5. When would you choose Global Accelerator over Route 53 for the front door? When you need sub-10-second failover (anycast backbone reroute, no DNS TTL), static IPs (firewall allow-lists, hard-coded device addresses), non-HTTP protocols (TCP/UDP), or lower latency/jitter via early backbone entry. Route 53 is cheaper and offers richer geo/weighted policies but fails over at DNS speed (60 s–minutes) and is subject to client caching.

6. What is static stability and why does it matter for failover? Static stability means Region B can serve — and take on more load — using only resources and control planes that already exist and are healthy, without depending on the impaired Region. It matters because if failing over to B requires calling an API in the sick Region A (to scale, promote, or change DNS), then A’s impairment can block your escape. It drives pre-provisioned headroom, raised quotas, and data-plane-only failover (ARC, GA dials).

7. What does Application Recovery Controller add over Route 53 health checks? ARC provides routing controls — deterministic on/off switches (backed by a five-Region cluster, so the control plane is itself Region-independent) you flip to shift traffic, ideal for gray failures and planned failovers where health checks flap or stay green — and readiness checks that continuously audit whether the recovery Region is actually provisioned to match production, catching capacity/quota drift before an incident.

8. Describe the three write topologies and which data each suits. Single global writer (all writes to one Region) — relational/constraint data, no conflicts, but far-user write latency and a promotion on failover. Multi-writer region-pinned (partition the keyspace so each item is written from one Region) — partitionable per-tenant/user data, local writes and no conflicts. Full multi-writer (every Region writes everything, resolve by LWW/merge) — conflict-free/tolerant event data, lowest latency, but demands the data model tolerate conflicts.

9. Why is idempotency mandatory in a multi-region design? Failover and client retries produce duplicate (and re-ordered) writes — a client retrying against Region B after A timed out may submit the same operation twice. Idempotent writes (conditional on a business key with TTL) turn duplicates into no-ops; without them you double-charge and double-ship. It is required, not optional, given at-least-once semantics.

10. What are the two gotchas with S3 Cross-Region Replication people miss? First, CRR replicates new objects only — it does not back-fill pre-existing objects (needs S3 Batch Replication) and does not replicate delete markers or lifecycle actions unless configured. Second, replication is eventually consistent with variable lag; use Replication Time Control (RTC) for a 15-minute SLA and metrics. A Multi-Region Access Point gives a single endpoint with failover controls on top.

11. How do you know what an unplanned failover will cost in data loss, before you trigger it? Watch the replication lag metric on every stream — AuroraGlobalDBReplicationLag (ms), DynamoDB ReplicationLatency, S3 OperationsPendingReplication. That lag is the RPO at that instant, so the current lag tells you how much data an unplanned failover loses. Alarm at RPO/2 so you can decide, with eyes open, whether to accept the loss.

12. Why does active-active often cost more than 2× a single Region? Because each Region must carry failover headroom — enough capacity to absorb the other Region’s traffic on failover — on top of its own steady-state load, so you pay for roughly 2× your peak capacity, not 2× your average. Add cross-region data-transfer, doubled data-store storage/replication-write costs, and (optionally) Global Accelerator, and the total exceeds a simple doubling. This is the reason warm standby is often the wiser choice.

These map to AWS Certified Solutions Architect – Professional (design for organizational complexity; resilient, high-availability, multi-Region architectures; DR strategies and RTO/RPO), AWS Certified Solutions Architect – Associate (design resilient architectures; Route 53 routing policies; RDS/Aurora/DynamoDB/S3 replication), and the AWS Certified Advanced Networking – Specialty (Route 53, Global Accelerator, cross-Region connectivity). A compact cert-mapping for revision:

Question theme Primary cert Objective area
DR tiers, RTO/RPO, switchover vs failover SAP-C02 Design for resiliency & business continuity
Route 53 policies & health checks SAA-C03 / ANS-C01 Design resilient / networked architectures
Global Accelerator vs DNS ANS-C01 Edge & global network design
Global tables, LWW, MRSC SAP-C02 / DBS Multi-Region data & consistency
Aurora Global DB, write-forwarding SAP-C02 / DBS Relational HA across Regions
Static stability, quotas, game days SAP-C02 Operational excellence & resilience

Quick check

  1. Your contract says RPO 0. A Regional impairment hits and you run failover-global-cluster --allow-data-loss on Aurora. Do you actually meet RPO 0? Why or why not?
  2. Two Regions write the same DynamoDB item within the same second. What happens to the two writes, and what signal (if any) do you get?
  3. You fail over with Route 53 health checks and some users still hit the dead Region ten minutes later. Name the two most likely reasons.
  4. What is the one metric that tells you, at any instant, how much data an unplanned failover would lose — and where do you set the alarm?
  5. Your standby Region is sized to its own steady-state traffic. Why will it likely fail when it must take the other Region’s load, and what are the two fixes?

Answers

  1. No. RPO 0 holds only for a planned managed switchover, which drains in-flight replication. failover-global-cluster --allow-data-loss is the unplanned path — it cannot drain the impaired Region, so its RPO equals the replication lag at the moment of failure (hopefully sub-second, which is why you alarm on lag, but not zero).
  2. The write with the higher timestamp survives; the other is silently discarded by last-writer-wins. You get no error and no notification — which is why a conflict canary and ReplicationLatency awareness matter, and why shared mutable items shouldn’t be full multi-writer.
  3. (a) The DNS record’s TTL is too high and/or client resolvers are caching the old answer, so clients don’t re-resolve to the healthy Region for minutes. (b) Some clients ignore TTL entirely. The fix is a low TTL plus, for real speed, Global Accelerator (anycast) or ARC routing controls that don’t depend on DNS propagation.
  4. Replication lagAuroraGlobalDBReplicationLag for Aurora, ReplicationLatency for DynamoDB, OperationsPendingReplication for S3. It is the live RPO. Alarm at RPO/2 on every replication stream so you can decide before failing over whether the current lag (data loss) is acceptable.
  5. It can’t absorb an instant 2× surge: autoscaling is too slow for the instant surge (and may itself be impaired during a Regional event), and the standby may hit default per-Region quotas. The two fixes: pre-provision failover headroom (or large warm pools) so failover is a routing change not a scaling race, and raise per-Region quotas to full production load in both Regions (with ARC readiness checks catching drift).

Glossary

Next steps

You can now design, reason about, and rehearse an active-active architecture. Build outward:

AWSMulti-RegionRoute 53DynamoDB Global TablesAurora Global DatabaseDisaster RecoveryGlobal AcceleratorApplication Recovery Controller
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading