The situation
A SaaS company runs its primary workload on Aurora PostgreSQL in us-east-1. The cluster holds 2 TB and sustains around 5,000 writes per second at peak. A compliance and ransomware-resilience review has landed five requirements. A cross-region recoverable copy in eu-west-1. RPO under a minute, meaning at most sixty seconds of writes lost in a regional disaster. RTO under an hour, from declaring failover to eu-west-1 accepting writes. Managed failover rather than a hand-driven runbook. And the secondary region serving low-priority reads during normal operations rather than sitting idle.
The existing architecture is single-region. What needs deciding is which cross-region mechanism to add, and how to configure it.
What actually matters
At 5,000 writes per second, a sixty-second RPO window holds 300,000 writes. Whatever moves data between regions has to keep pace with that rate continuously rather than catch up on a schedule. A backup job that runs hourly cannot produce sub-minute RPO by construction. Asynchronous log shipping with a typical lag quoted in seconds widens to minutes under a write spike, so a typical figure is not a bound. Something has to make the ceiling explicit, or the requirement is aspirational.
The sub-hour RTO depends on the shape of the recovery operation. Restore the latest snapshot, wait for it, edit the application config, wire up IAM, rotate secrets, update DNS. That is an hour on a good day and several on a bad one. Every step is human-timed and the slow ones run in sequence. A single API call that promotes an already-current secondary takes minutes. What removes the restore step is the secondary already being a live, queryable database.
A readable secondary changes what the second region is worth. A cluster the business queries for reports and analytics is a line item with something to show for it. A cluster that does nothing until a disaster is a line item finance eventually questions, and an idle region is where configuration drift hides until the day it matters.
Managed and self-built replication differ in what the team owes after launch. A CDC pipeline can meet every other requirement if somebody builds it, and then it has to keep meeting them through schema changes, engine upgrades, capacity growth and staff turnover. AWS-operated plumbing costs instance hours and I/O charges. Team-operated plumbing costs those plus an on-call rotation that understands it. The usual trade is flexibility, and for homogeneous Aurora-to-Aurora replication there is nothing to be flexible about.
Scale matters because the same growth curve puts 2 TB near 4 TB inside two years. Storage-layer replication built for Aurora’s internal throughput absorbs that without attention. A CDC pipeline sized for today’s write rate gets re-tuned every few quarters.
Then a sixth property that no requirements document carries: what happens after the recovery. The old primary region comes back eventually. Whether it rejoins on its own or has to be rebuilt by hand decides how the day after the incident goes. So does whether the writes lost at the moment of failure are recoverable at all.
What we’ll filter on
- Sub-minute RPO under sustained write load, with a mechanism that bounds it rather than a typical figure.
- Sub-hour RTO through a single managed operation rather than a human-driven runbook.
- A secondary that serves reads during normal operations.
- Managed replication, with no CDC pipeline for the team to own.
- Multi-TB scale without per-quarter tuning.
- The original topology restored automatically after a failover.
The landscape
Aurora Global Database. One global cluster spans regions: a primary Aurora DB cluster taking writes, plus up to 10 read-only secondary clusters, each in a different region and each hosting its own readers. Replication runs through the storage layer over dedicated infrastructure rather than through the database engine, at a latency AWS describes as typically under a second. AWS documents RPO for the feature in seconds and RTO in minutes, and on Aurora PostgreSQL the rds.global_db_rpo parameter converts that typical RPO into an enforced ceiling. Secondaries are readable by default, and write forwarding sends writes issued against a secondary’s reader endpoint to the primary.
Cross-region read replicas. Aurora MySQL can create a read replica in another region through MySQL binary log replication. Up to five per source cluster, each promotable to a standalone writable cluster. Lag tracks write volume and network conditions, promotion detaches the replica from its source permanently, and the cutover is a runbook rather than one call. For this fleet it is not on the table at all. Aurora PostgreSQL does not support cross-region Aurora Replicas, and AWS’s documented answer for cross-region Aurora PostgreSQL is a global database. RDS for PostgreSQL has cross-region replicas; Aurora PostgreSQL does not.
AWS Database Migration Service with ongoing CDC. The general continuous-replication tool. It is the right one where the target is a different engine or a different shape, such as Amazon Redshift or S3 for analytics. For homogeneous Aurora-to-Aurora it means assembling by hand what a global database provides as a feature: replication instances to size and patch, tasks to author and maintain, schema changes to coordinate at both ends.
AWS Backup cross-region copy. A backup plan takes periodic snapshots and copies them to a vault in the destination region, under Vault Lock, and into another account’s vault if the plan says so. AWS Backup does support continuous backups with point-in-time recovery for Aurora, at an RPO AWS describes as typically under five minutes. Those continuous recovery points cannot be copied anywhere, because AWS Backup does not copy transaction logs. It creates a snapshot and copies that instead, and snapshots run hourly at the fastest. The RPO of the copy in the destination region is therefore the snapshot interval, and nothing there is queryable between restores. Aurora continuous backups also cannot use a logically air-gapped vault, so a ransomware design built around one runs on periodic snapshots. This covers the compliance and ransomware half of the review, and it belongs alongside live replication rather than instead of it.
Evaluation
Side by side
| Mechanism | RPO < 1 min | RTO < 1 hr managed | Readable secondary | Operationally managed | Scales to multi-TB | Topology restored |
|---|---|---|---|---|---|---|
| Aurora Global Database | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Cross-region read replica (Aurora MySQL only) | Partial | ✗ | ✓ | Partial | ✓ | ✗ |
| DMS ongoing CDC | Partial | ✗ | ✓ | ✗ | ✓ | ✗ |
| AWS Backup cross-region copy | ✗ | ✗ | ✗ | ✓ | ✓ | n/a |
One row clears all six filters. On Aurora PostgreSQL the second row is not selectable whatever its ticks say, which leaves the configuration questions rather than the choice.
Matching requirements to mechanism
The solution
A global cluster is the top-level object. Inside it sit one primary Aurora DB cluster and up to 10 secondary clusters, each in a distinct region. Each is an ordinary Aurora cluster with its own instances, endpoints, parameter groups, security groups and IAM roles. What the global cluster adds is replication between the primary’s storage volume and each secondary’s, plus two APIs that move the primary role between regions.
Aurora does not keep database data on an instance’s local disk; local storage holds only non-persistent temporary files. The engine writes redo log records to a shared cluster volume, which Aurora replicates synchronously to six storage nodes across three Availability Zones, and every reader in the cluster attaches to that same volume. For a global database, the primary’s storage volume ships those redo records to each secondary’s volume over dedicated AWS infrastructure. The secondary’s storage applies them and serves the results to its readers. The engine never executes a statement twice and never competes with a replication process for I/O. That is why AWS puts the replication latency at typically under a second, and says replication has little performance impact on the primary because the DB instances’ resources stay devoted to application reads and writes. AWS does not publish that latency against any particular write rate, so it is a typical measurement rather than a figure that holds at 5,000 writes per second.
Bounding the RPO instead of hoping for it
Typical lag under a second is a measurement, not a guarantee, and this scenario asks for a guarantee. On Aurora PostgreSQL, rds.global_db_rpo sets an upper bound in seconds, with a minimum of 20. Aurora tracks RPO lag for every secondary and commits a transaction while at least one secondary is inside the bound. When every secondary is further behind than that, Aurora blocks the commit, logs it, and emits wait events for the stalled sessions until one catches up.
Set it to 30 for this scenario and the sixty-second requirement becomes a limit rather than an observation. What changes is the kind of risk, not the amount of it: unbounded data loss becomes bounded data loss plus a write stall whenever replication falls behind. With exactly one secondary there is nothing else to satisfy the bound. A single lag spike in eu-west-1 stalls writes in us-east-1, which argues either for a looser bound or for a second secondary region carrying the same data. It is the same parameter that carries the bound in a multi-region design built to fail over inside a minute. Two smaller notes. Set it on the primary’s parameter group and leave the secondary’s at the default. In a two-region global database, an RPO enforced in the secondary pauses transactions after a failover until Aurora finishes rebuilding the old primary. And a major version upgrade of Aurora PostgreSQL is refused while the parameter is on, so an upgrade means resetting it first.
The secondary during normal operations
Each secondary hosts its own readers behind its own reader endpoint. Applications in eu-west-1 read locally instead of crossing the Atlantic, against data typically under a second stale. A read-only secondary cluster takes up to 16 readers where an ordinary cluster takes 15. Two quotas bite the other way. The primary cluster’s reader limit falls by one for every secondary region, to 15 minus the number of secondaries, so a single secondary leaves room for 14 readers on the primary. Aurora Auto Scaling does not work on secondary clusters either, so reader count there is a decision rather than a policy. Aurora Serverless v2 is allowed for those readers. That is the lever for a secondary read hard in business hours and nearly idle overnight. AWS’s 8-ACU minimum-capacity recommendation for a serverless global database applies to the primary region only.
Write forwarding is off by default and turned on per secondary cluster with --enable-global-write-forwarding. A session connected to the secondary’s reader endpoint issues a write, Aurora forwards the statement to the primary, executes it there, and replicates the change back. Consistency is per session, through apg_write_forward.consistency_mode. SESSION is the default: the session sees its own writes, waiting for them to replicate back, but not other sessions’. EVENTUAL waits for nothing and may read stale. GLOBAL waits until the secondary is current with every committed change anywhere in the global cluster. OFF disables forwarding in that session. Every minor version of Aurora PostgreSQL 16 and above supports forwarding, as do 15.4 and 14.9 and their higher minors.
Forwarding covers DML, the SELECT FOR UPDATE family, PREPARE and EXECUTE. It does not cover DDL, COPY, TRUNCATE, VACUUM, ANALYZE, CLUSTER, LOCK, SAVEPOINT, SELECT INTO, GRANT and REVOKE, cursors, user-defined functions or user-defined procedures, and SERIALIZABLE isolation is unavailable in a forwarding session. Forwarded sessions also draw on the primary’s connection pool, capped by default at 25% of the writer’s max_connections. An application whose writes are occasional and whose reads are local fits this; a write-heavy path does not, because every forwarded statement still crosses the Atlantic twice.
Switchover against failover
switchover-global-cluster is the planned operation, against the global cluster identifier with the target secondary’s ARN. Aurora waits for the target secondary to synchronise fully, makes the primary read-only, then promotes a reader on the target to writer. RPO is zero by construction. It is the mechanism for regional rotation, planned maintenance, follow-the-sun writes and DR drills, and it requires every cluster in the global database healthy, which rules it out of a real outage.
failover-global-cluster --allow-data-loss is the unplanned operation, and the flag is the whole difference: it does not wait for synchronisation, so the loss is whatever had not replicated when the primary region stopped. AWS puts the promotion at typically a few minutes.
Three mechanics decide whether that number is real. Both operations require the primary and secondary to run the same major and minor engine version, with patch-level tolerance varying by engine version. An upgrade that drifts one cluster ahead of the other turns the managed path into the manual one, in which somebody detaches the secondary by hand. Aurora attempts to fence writes at the storage layer during a failover, and emits an RDS event saying whether it succeeded. Fencing is best-effort. Writes can briefly land in the old region, which is the split-brain case AWS names. The recommended preparation is unglamorous. Take applications offline first. Connect through the Aurora Global Database writer endpoint, which follows the primary across both operations. Drop the DNS cache TTL on it to around five seconds, so the change propagates before write traffic resumes.
What it costs is a second cluster’s instance hours, storage in the second region, and replicated write I/Os between the primary and each secondary. That last one is charged at the standard I/O rate, USD$0.20 per million in AWS’s own published pricing example. Aurora I/O-Optimized does not waive it, even though it waives ordinary read and write I/Os. A second secondary region, added for the reason above, adds another full copy of that replication charge. That is the number to put in front of whoever asked for the bound.
Worked example
14:02 UTC on a Tuesday. us-east-1 is degraded, eu-west-1 is healthy, and the platform team has declared a regional failover.
Steady state beforehand. Primary prodbox-us in us-east-1 with one writer and two readers, secondary prodbox-eu in eu-west-1 with two readers, rds.global_db_rpo set to 30 seconds on the primary’s parameter group. RPO lag averages around 400 ms. Analytics already run against the eu-west-1 reader endpoint, and the application writes through the global writer endpoint.
T+0. The incident commander authorises failover. Applications are taken offline to stop writes reaching us-east-1, and failover-global-cluster --allow-data-loss runs against prodbox-global naming the eu-west-1 cluster’s ARN as the target.
T+0 to T+a few minutes. The eu-west-1 cluster promotes one of its readers to writer and takes over the primary role. Aurora fences writes in the old region and emits the RDS event confirming it. Whatever had not replicated is lost. At 5,000 writes per second and roughly 400 ms of lag, that is about two thousand writes, and the 30-second bound caps the worst case at 150,000.
T+minutes. The global writer endpoint now resolves to the eu-west-1 writer, so there is no connection string to edit and no CNAME to swing. The team confirms the DNS change has propagated, watching for the RDS event that says Aurora observed it, then brings the application back up. Sessions that had been writing through the eu-west-1 reader endpoint need moving. Write forwarding is a property of a secondary cluster, and GlobalWriteForwardingStatus reads null once a cluster holds the primary role, so those writes go to the global writer endpoint like every other write. The promoted cluster’s own regional endpoint did change, losing its -ro infix, and that is the edit the global endpoint removes.
T+20 minutes or so, recovered. RPO is the two thousand writes at T+0. RTO is ten to twenty minutes of wall clock, three to six times inside the one-hour ceiling.
Afterwards. When us-east-1 is healthy again, Aurora adds it back to the global cluster as a secondary on its own and builds it a new storage volume. The original topology returns without anybody filing a ticket. Before building that volume, Aurora attempts a snapshot of the old volume as it stood at the moment of failure, named rds:unplanned-global-failover- plus the old cluster’s name and a timestamp. That snapshot is where the two thousand lost writes can be read back from. It is a system snapshot subject to the old cluster’s backup retention, so copying it to a manual snapshot is the first thing to do if anyone will want it later. Had there been further secondary regions, Aurora would rebuild each of them from the new primary too. That takes minutes to several hours depending on volume size and distance, and they serve no reads until it finishes. A switchover-global-cluster at a quiet moment weeks later returns the primary role to us-east-1 at zero RPO.
What’s worth remembering
- Global Database replicates via storage. Typical lag is under a second; RPO is documented in seconds, RTO in minutes, across up to 10 secondary regions.
- No cross-region replicas on Aurora PostgreSQL. A global database is AWS’s documented answer; only Aurora MySQL has binary log cross-region read replicas.
- Enforce RPO with
rds.global_db_rpo. The minimum is 20 seconds; Aurora blocks commits on the primary whenever every secondary lags beyond the bound. - Switchover is planned; failover loses data. Switchover gives zero RPO and needs every cluster healthy;
failover-global-cluster --allow-data-lossloses whatever had not replicated. - Match engine versions across clusters. Mismatched major or minor versions turn managed failover into manual detachment; the global writer endpoint needs no application change.
- Replicated write I/O is billed. Charged between primary and each secondary, and Aurora I/O-Optimized does not waive it, so every extra secondary region repeats it.