Go Multi-Region on Postgres with AWS Aurora Global Database
Build a two-region Aurora PostgreSQL global database and switch primary regions with zero data loss.
What you'll build
A two-region Amazon Aurora PostgreSQL global database: a writable primary cluster in us-east-1, a read-only secondary in us-west-2 fed by storage-level replication that typically lags well under a second, and a controlled switchover that promotes the secondary to primary with zero data loss and only a brief write pause.
flowchart LR
A[App] -->|writes| G[Global writer endpoint]
G --> P[(Primary cluster us-east-1)]
P -. async storage replication, typically under 1s .-> S[(Secondary cluster us-west-2)]
S -->|local reads| R[Regional readers]
Prerequisites
Verified against Aurora PostgreSQL 17.7 (the current long-term-support release; 17.9 is the newest minor) and AWS CLI v2 on macOS/Linux.
- An AWS account with rights to manage RDS and EC2 security groups in
us-east-1andus-west-2, each region with its default VPC intact. - AWS CLI v2 — any recent build; run
aws rds switchover-global-cluster helpto confirm yours has the subcommand (added in 2023). - psql 15 or newer.
- Budget: two
db.r6g.largeinstances (roughly US$0.52/hour combined, on-demand) plus storage and cross-region replication I/O. Global databases require memory-optimizeddb.r*instance classes — burstabledb.t*classes won't work. Teardown commands are in Next steps. - This lab makes both instances publicly accessible and opens port 5432 to your IP so psql works from your machine. Don't do that in production — use private subnets and a bastion or SSM tunnel.
1. Create the global cluster container
A global database starts as an empty container object that regional clusters attach to. Pin the engine version — primary and secondary must run identical major and minor versions for managed switchover.
aws rds create-global-cluster \
--region us-east-1 \
--global-cluster-identifier globaldemo \
--engine aurora-postgresql \
--engine-version 17.7
If 17.7 isn't offered in your region, list what is and substitute throughout:
aws rds describe-db-engine-versions --engine aurora-postgresql \
--region us-east-1 --query 'DBEngineVersions[].EngineVersion' --output text
2. Create the primary cluster in us-east-1
Create the cluster inside the container, then give it a writer instance. Only the primary takes master credentials (pick your own password).
aws rds create-db-cluster \
--region us-east-1 \
--db-cluster-identifier globaldemo-use1 \
--engine aurora-postgresql \
--engine-version 17.7 \
--master-username postgres \
--master-user-password 'S3curePassw0rd' \
--global-cluster-identifier globaldemo
aws rds wait db-cluster-available --region us-east-1 \
--db-cluster-identifier globaldemo-use1
aws rds create-db-instance \
--region us-east-1 \
--db-cluster-identifier globaldemo-use1 \
--db-instance-identifier globaldemo-use1-writer \
--db-instance-class db.r6g.large \
--engine aurora-postgresql \
--publicly-accessible
aws rds wait db-instance-available --region us-east-1 \
--db-instance-identifier globaldemo-use1-writer
The instance takes 5–10 minutes; the waiter blocks until it's ready.
3. Attach the secondary region
Same two calls in us-west-2, with one crucial difference: pass the global cluster ID but no credentials — the secondary replicates them from the primary. If your primary is encrypted, also pass --kms-key-id (KMS keys are regional) and --source-region us-east-1.
aws rds create-db-cluster \
--region us-west-2 \
--db-cluster-identifier globaldemo-usw2 \
--engine aurora-postgresql \
--engine-version 17.7 \
--global-cluster-identifier globaldemo
aws rds wait db-cluster-available --region us-west-2 \
--db-cluster-identifier globaldemo-usw2
aws rds create-db-instance \
--region us-west-2 \
--db-cluster-identifier globaldemo-usw2 \
--db-instance-identifier globaldemo-usw2-reader \
--db-instance-class db.r6g.large \
--engine aurora-postgresql \
--publicly-accessible
aws rds wait db-instance-available --region us-west-2 \
--db-instance-identifier globaldemo-usw2-reader
A secondary cluster without an instance is "headless" — cheaper, but you can't switch over to it, hence the reader.
4. Seed data and check replication lag
Open 5432 to your IP in each region's default security group, then write to the primary:
MYIP=$(curl -s https://checkip.amazonaws.com)
for r in us-east-1 us-west-2; do
aws ec2 authorize-security-group-ingress --region $r \
--group-name default --protocol tcp --port 5432 --cidr ${MYIP}/32
done
PRIMARY_EP=$(aws rds describe-db-clusters --region us-east-1 \
--db-cluster-identifier globaldemo-use1 \
--query 'DBClusters[0].Endpoint' --output text)
psql "host=$PRIMARY_EP user=postgres password=S3curePassw0rd dbname=postgres" <<'SQL'
CREATE TABLE canary (id bigserial PRIMARY KEY, note text, at timestamptz DEFAULT now());
INSERT INTO canary (note) VALUES ('written in us-east-1');
SELECT aws_region, durability_lag_in_msec, rpo_lag_in_msec FROM aurora_global_db_status();
SQL
aurora_global_db_status() reports per-region lag from the primary's point of view (-1 marks the primary itself):
aws_region | durability_lag_in_msec | rpo_lag_in_msec
------------+------------------------+-----------------
us-east-1 | -1 | -1
us-west-2 | 142 | 308
Low hundreds of milliseconds or less on the secondary is normal — that's your sub-second replication. The same signal feeds the AuroraGlobalDBRPOLag CloudWatch metric on the secondary cluster, which is what you alarm on in production. Confirm lag is low before switching over: switchover duration is proportional to it.
5. Switch over to us-west-2
switchover-global-cluster is the managed, planned path (formerly "managed planned failover"): Aurora waits for the target secondary to fully sync, makes the old primary read-only, then promotes the target — RPO 0 by design. The unplanned-disaster command is failover-global-cluster --allow-data-loss; don't confuse the two. Note --region is the current primary's region and the target must be an ARN:
TARGET_ARN=$(aws rds describe-db-clusters --region us-west-2 \
--db-cluster-identifier globaldemo-usw2 \
--query 'DBClusters[0].DBClusterArn' --output text)
aws rds switchover-global-cluster \
--region us-east-1 \
--global-cluster-identifier globaldemo \
--target-db-cluster-identifier "$TARGET_ARN"
Poll while roles swap — with lag this low, expect the write pause to last on the order of a minute:
aws rds describe-global-clusters --region us-east-1 \
--global-cluster-identifier globaldemo \
--query 'GlobalClusters[0].{Status:Status,Members:GlobalClusterMembers[].{Cluster:DBClusterArn,Writer:IsWriter}}'
Status goes available → switching-over → available. If your app connects through the global writer endpoint (the Endpoint field of describe-global-clusters, shaped like globaldemo.global-<id>.global.rds.amazonaws.com), it follows the writer automatically — no connection-string change, just keep client DNS TTLs low.
Verify it works
When the switchover finishes, the poll from step 5 shows the writer flag flipped:
{
"Status": "available",
"Members": [
{ "Cluster": "arn:aws:rds:us-east-1:123456789012:cluster:globaldemo-use1", "Writer": false },
{ "Cluster": "arn:aws:rds:us-west-2:123456789012:cluster:globaldemo-usw2", "Writer": true }
]
}
Confirm zero data loss and write capability in the new primary:
NEW_EP=$(aws rds describe-db-clusters --region us-west-2 \
--db-cluster-identifier globaldemo-usw2 \
--query 'DBClusters[0].Endpoint' --output text)
psql "host=$NEW_EP user=postgres password=S3curePassw0rd dbname=postgres" \
-c "SELECT note FROM canary;" \
-c "INSERT INTO canary (note) VALUES ('written in us-west-2') RETURNING id;"
Expected output:
note
----------------------
written in us-east-1
(1 row)
id
----
2
(1 row)
The pre-switchover row survived and writes now land in us-west-2. The demoted us-east-1 cluster still serves reads but rejects writes — an INSERT against its endpoint returns:
ERROR: cannot execute INSERT in a read-only transaction
Troubleshooting
InvalidParameterCombination ... Cannot specify user name for cross region replication cluster — you passed --master-username/--master-user-password when creating the secondary cluster. Secondaries inherit credentials via replication; drop both flags (see step 3).
InvalidGlobalClusterStateFault: The global cluster is in an invalid state and can't perform the requested operation on switchover — the global cluster isn't available yet (a member is still creating or modifying), or the clusters run different engine versions. Wait for Status: available in describe-global-clusters, confirm both clusters report the same EngineVersion, and retry.
psql: error: connection to server at "….rds.amazonaws.com" …, port 5432 failed: Connection timed out — the instance isn't publicly accessible or the security group doesn't allow your IP. Re-run the authorize-security-group-ingress loop from step 4 (your IP may have changed), or run psql from a host inside that region's VPC.
InvalidDBClusterStateFault: This cluster is a part of a global cluster, please remove it from globalcluster first — during teardown, a member cluster must be detached with remove-from-global-cluster before delete-db-cluster, and the detach takes a few seconds to land. Wait briefly and retry the delete.
Next steps
First, tear the lab down — detach secondaries before the primary:
USE1_ARN=$(aws rds describe-db-clusters --region us-east-1 \
--db-cluster-identifier globaldemo-use1 --query 'DBClusters[0].DBClusterArn' --output text)
aws rds remove-from-global-cluster --region us-east-1 \
--global-cluster-identifier globaldemo --db-cluster-identifier "$USE1_ARN"
aws rds delete-db-instance --region us-east-1 --db-instance-identifier globaldemo-use1-writer
aws rds delete-db-cluster --region us-east-1 \
--db-cluster-identifier globaldemo-use1 --skip-final-snapshot
# repeat the four commands for us-west-2 (globaldemo-usw2 / -reader), then:
aws rds delete-global-cluster --region us-east-1 --global-cluster-identifier globaldemo
To go deeper: enforce an RPO ceiling with the rds.global_db_rpo cluster parameter (Aurora blocks commits on the primary if no secondary is within it); run an unplanned-failover drill with failover-global-cluster --allow-data-loss and watch Aurora rebuild the old primary as a secondary; evaluate write forwarding for occasional writes from secondary regions; and wire CloudWatch alarms on AuroraGlobalDBRPOLag before taking this to production. All four are covered in the Aurora Global Database guide.
Sources & further reading
- Creating an Amazon Aurora global database — docs.aws.amazon.com
- Adding an AWS Region to an Amazon Aurora global database — docs.aws.amazon.com
- Using switchover or failover in Amazon Aurora Global Database — docs.aws.amazon.com
- Monitoring an Amazon Aurora global database — docs.aws.amazon.com
- Connecting to Amazon Aurora Global Database — docs.aws.amazon.com
- Amazon Aurora now supports PostgreSQL 17.9, 16.13, 15.17, and 14.22 — aws.amazon.com
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.