Business Challenge
A financial services platform processes transactions in us-east-1. The compliance team requires a documented RTO of under 5 minutes and RPO of under 1 minute for the primary database tier. The platform team sets up Aurora Global Database with a secondary cluster in eu-west-1, runs a DR drill, promotes the secondary in 47 seconds, and files the compliance report. The architecture works.
Six months later, us-east-1 suffers a real 3-hour outage. The team promotes eu-west-1 in under a minute. Transactions resume. The RTO target is met. Then the primary region recovers β and nobody knows what to do next. The old primary is now a standalone cluster with 3 hours of write history that diverged the moment eu-west-1 was promoted. The new primary has 3 hours of production writes on top of that. There is no automatic merge. There is no automatic rejoin. The global cluster is permanently broken until someone rebuilds it manually.
The DR plan covered failover. It did not cover failback. This is the gap that makes Aurora Global Database implementations incomplete in most enterprises.
Most teams write the failover runbook and consider the job done. Failback β re-attaching the recovered region as a secondary, reconciling diverged data, and running a second planned switchover β requires its own documented procedure and must be tested before an incident forces you to improvise it.
Architecture
Aurora Global Database replicates at the storage layer, not the SQL layer. The primary region maintains a writer instance and optional local readers. Up to ten secondary regions each maintain a read-only cluster. Replication is asynchronous but fast β typical lag is under one second because it operates on physical redo log segments, not logical SQL statements.
Endpoint strategy
Applications must never hard-code Aurora cluster endpoints. Instead, Route 53 aliases sit in front of the cluster endpoints. During normal operation, write.db.internal points to the primary cluster writer endpoint. During failover, you update the Route 53 record to point to the promoted secondary. Applications reconnect to the same DNS name β no application config change required.
Read traffic uses a separate read.db.internal record with latency-based routing. EU users are served by the eu-west-1 reader cluster; US users by the us-east-1 reader endpoint. This gives you read scalability and geographic latency reduction without any application awareness of the global topology.
Write forwarding
Aurora Global Database supports write forwarding on secondary clusters. When enabled, write statements issued against the secondary are transparently forwarded to the primary writer. This simplifies application architecture β a single connection string works for both reads and writes in any region β but adds latency to writes from the secondary region (round-trip to primary plus replication lag back). Write forwarding is appropriate for low-volume, non-latency-sensitive writes from the secondary region, not for high-throughput write paths.
Why This Architecture Works
Storage-level replication beats logical replication
Aurora replicates redo log segments, not SQL statements. This means replication lag is measured in milliseconds regardless of transaction size, and the secondary does not re-execute queries. A 10GB bulk load that takes 2 minutes in the primary is visible in the secondary within 1 second of each storage write completing.
Secondary clusters serve real read traffic
This is not a cold standby. The secondary cluster actively handles production read queries with local latency. EU users reading account balances get sub-10ms query times from eu-west-1 instead of crossing the Atlantic to us-east-1. The DR capacity is earning its keep every day.
Planned switchover has zero data loss
When you initiate a planned switchover, Aurora drains in-flight writes from the primary before promoting the secondary. The replication lag reaches zero before the role switch completes. The old primary becomes a reader. No data is lost, no reconciliation is needed, and the global cluster remains intact.
RTO under 1 minute for managed failover
For planned switchover, Aurora handles the promotion automatically and the entire operation completes in under 1 minute. For unplanned failover, detaching and promoting the secondary is a single API call that typically completes in 1β2 minutes. Compare this to multi-AZ read replica promotion, which can take 5β10 minutes.
Key Design Decisions
Many teams run the secondary at a smaller instance class to save cost β a db.r6g.large secondary behind a db.r6g.2xlarge primary. This works until failover happens and the secondary is suddenly handling full production write throughput on an undersized instance. The secondary must match the primary's write capacity, not its read capacity.
Size the secondary writer at the same instance class as the primary writer. You can run fewer reader instances in the secondary to reduce cost, but the promoted writer must be able to absorb peak write load immediately.
After an unplanned failover, the promoted secondary is a standalone cluster. When the original primary region recovers, it comes back as a standalone cluster with diverged data from the outage period. You cannot simply re-attach it. You must: snapshot the recovered primary, create a new Aurora cluster from the snapshot in the secondary role, attach it to the global cluster with the current promoted cluster as primary, then run a planned switchover back.
correct approachDocument and test the failback procedure as a separate runbook. Include data reconciliation steps for any writes that occurred on the old primary during the outage if the region recovered while still in an inconsistent state.
When the writer endpoint changes during failover, all application connections drop simultaneously. If the application opens connections directly to the Aurora endpoint, a few hundred application instances attempting to reconnect at the same moment can overwhelm the promoted writer before it has finished stabilising.
correct approachPlace RDS Proxy in front of the writer in each region. Proxy maintains a persistent connection pool to Aurora and presents a stable endpoint to applications. During failover, Proxy re-establishes its pool connections to the new writer without the application layer experiencing a mass reconnect storm.
Some teams try to fully automate failover β CloudWatch alarm triggers Lambda which calls the promotion API and updates Route 53. This creates false-positive failover risk: a brief CloudWatch metrics gap triggers promotion unnecessarily, and now you have a broken global cluster to repair from a non-event.
correct approachKeep the promotion decision human. Automate the Route 53 record update and notification only. An on-call engineer confirms the regional failure is real before initiating promotion. The promotion itself takes under 2 minutes β the decision confirmation is the only meaningful delay.
Tradeoffs
| Decision | Benefit | Cost / Risk |
|---|---|---|
| Aurora Global Database vs Multi-AZ | Cross-region DR, read scaling, <1s RPO | 2Γ instance cost, failback complexity |
| Write forwarding enabled | Single connection string, simpler app logic | Added write latency (cross-region round trip) |
| RDS Proxy in front of writer | Connection pooling, cleaner failover | Additional cost (~$0.015/hr per AZ), extra hop |
| Planned switchover for maintenance | Zero data loss, global cluster stays intact | Requires coordination, brief write interruption |
| Automated promotion on alarm | Faster recovery, no human in the loop | False positives cause unnecessary failover and data reconciliation work |
Terraform Pattern
The key resource is aws_rds_global_cluster β this is the global cluster object that the primary and secondary regional clusters attach to.
# Global cluster object (region-agnostic)
resource "aws_rds_global_cluster" "main" {
global_cluster_identifier = "platform-global"
engine = "aurora-postgresql"
engine_version = "16.4"
database_name = "platform"
storage_encrypted = true
}
# Primary cluster β us-east-1
resource "aws_rds_cluster" "primary" {
provider = aws.us_east_1
cluster_identifier = "platform-primary"
engine = aws_rds_global_cluster.main.engine
engine_version = aws_rds_global_cluster.main.engine_version
global_cluster_identifier = aws_rds_global_cluster.main.id
database_name = "platform"
master_username = var.db_username
master_password = var.db_password
db_subnet_group_name = aws_db_subnet_group.primary.name
vpc_security_group_ids = [aws_security_group.aurora_primary.id]
kms_key_id = aws_kms_key.aurora_primary.arn
storage_encrypted = true
skip_final_snapshot = false
final_snapshot_identifier = "platform-primary-final"
}
resource "aws_rds_cluster_instance" "primary_writer" {
provider = aws.us_east_1
identifier = "platform-primary-writer"
cluster_identifier = aws_rds_cluster.primary.id
instance_class = "db.r6g.2xlarge"
engine = aws_rds_cluster.primary.engine
engine_version = aws_rds_cluster.primary.engine_version
publicly_accessible = false
}
# Secondary cluster β eu-west-1
resource "aws_rds_cluster" "secondary" {
provider = aws.eu_west_1
cluster_identifier = "platform-secondary"
engine = aws_rds_global_cluster.main.engine
engine_version = aws_rds_global_cluster.main.engine_version
global_cluster_identifier = aws_rds_global_cluster.main.id
db_subnet_group_name = aws_db_subnet_group.secondary.name
vpc_security_group_ids = [aws_security_group.aurora_secondary.id]
kms_key_id = aws_kms_key.aurora_secondary.arn
storage_encrypted = true
skip_final_snapshot = false
final_snapshot_identifier = "platform-secondary-final"
depends_on = [aws_rds_cluster_instance.primary_writer]
}
resource "aws_rds_cluster_instance" "secondary_reader" {
provider = aws.eu_west_1
identifier = "platform-secondary-reader"
cluster_identifier = aws_rds_cluster.secondary.id
instance_class = "db.r6g.2xlarge" # match primary writer size
engine = aws_rds_cluster.secondary.engine
engine_version = aws_rds_cluster.secondary.engine_version
publicly_accessible = false
}
# Route 53 write endpoint alias
resource "aws_route53_record" "db_write" {
zone_id = var.private_zone_id
name = "write.db"
type = "CNAME"
ttl = 30
records = [aws_rds_cluster.primary.endpoint]
}
Closing Thought
Aurora Global Database solves the hard part of multi-region database architecture: keeping a secondary region current enough to take over production traffic within seconds, not hours. The replication lag target of under one second is not a marketing claim β it reflects the architecture's use of physical storage replication rather than logical query replay.
But the architecture only delivers its promise when the team has practised the full lifecycle. Failover without a tested failback plan is a trap: the drill passes, the compliance report is filed, and the real incident reveals a gap that takes days to close. Build both runbooks, test both paths, and size the secondary to absorb writes from day one β not from the moment a region goes down.
Most organisations look at their AWS bill once a month, after the fact. We look at how to build a self-service cost intelligence platform using Cost and Usage Report 2.0 and Athena that gives engineering teams real-time visibility into where spend is going and why.
Official AWS Reference
Amazon Aurora User Guide β Aurora Global Database
β covers global cluster creation, adding secondary regions, managed planned failover (switchover), unplanned failover, write forwarding configuration, and monitoring replication lag via the AuroraGlobalDBReplicationLag CloudWatch metric.
Comments