Skip to content
Cloud & Infra Advanced Tutorial

Build Cross-Region RDS Disaster Recovery with One-Command Failover

Stand up a cross-region RDS replica, replicated backups, and a promotion script so a region outage costs minutes, not data.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Sep 11, 2026 · 6 min read
Build Cross-Region RDS Disaster Recovery with One-Command Failover

What you'll build

A warm-standby disaster recovery setup for an Amazon RDS PostgreSQL instance: an encrypted cross-region read replica, automated backups replicated to the standby region, a CloudWatch alarm on replication lag, and a one-command failover script that promotes the replica to a standalone primary.

Prerequisites

  • AWS CLI v2, verified against 2.36.x (September 2026). Credentials need RDS, KMS, SNS, and CloudWatch permissions in both regions.
  • An existing RDS for PostgreSQL instance. This tutorial assumes appdb-prod in us-east-1, running PostgreSQL 17, encrypted, with automated backups enabled. Read replicas require automated backups on the source (BackupRetentionPeriod > 0).
  • The steps work for MySQL, MariaDB, SQL Server, Oracle, and Db2 too, with engine-specific caveats noted where they bite.
  • A shell on Linux or macOS, plus psql for the verification step.
  • Replace 123456789012 with your account ID throughout.

Cost warning before you start: you're paying for a second instance plus cross-region data transfer on every write and every replicated backup.

Step 1: Create a KMS key in the DR region

KMS keys are regional, so an encrypted replica in us-west-2 needs its own key there:

DR_KEY_ARN=$(aws kms create-key --region us-west-2 \
  --description "RDS DR key for appdb" \
  --query KeyMetadata.Arn --output text)
echo "$DR_KEY_ARN"

If your primary is unencrypted, skip this step and drop the --kms-key-id flags below. RDS can't create an encrypted replica from an unencrypted source.

Step 2: Create the cross-region read replica

Run create-db-instance-read-replica against the destination region and reference the source by ARN:

aws rds create-db-instance-read-replica \
  --region us-west-2 \
  --db-instance-identifier appdb-dr \
  --source-db-instance-identifier arn:aws:rds:us-east-1:123456789012:db:appdb-prod \
  --db-instance-class db.r6g.large \
  --kms-key-id "$DR_KEY_ARN"

Match or exceed the primary's instance class, because after a failover this instance takes the full production write load. Behind the scenes RDS snapshots the source, copies the snapshot cross-region, restores it, then starts streaming changes. Budget anywhere from 30 minutes to several hours depending on database size. Block until it's ready:

aws rds wait db-instance-available --region us-west-2 \
  --db-instance-identifier appdb-dr

Step 3: Replicate automated backups to the DR region

The replica alone is not enough. Replication applies a bad DELETE or a dropped table to the standby within seconds, so you also want point-in-time restore capability in the DR region. Enable cross-region automated backup replication, again running against the destination region:

aws rds start-db-instance-automated-backups-replication \
  --region us-west-2 \
  --source-db-instance-arn arn:aws:rds:us-east-1:123456789012:db:appdb-prod \
  --kms-key-id "$DR_KEY_ARN" \
  --backup-retention-period 7

From this point RDS copies every snapshot and transaction log to us-west-2 as soon as it lands, giving you cross-region PITR with a 7-day window. Not every region pair supports this; check with aws rds describe-source-regions --region us-west-2 and look for "SupportsDBInstanceAutomatedBackupsReplication": true on your source region. The us-east-1 to us-west-2 pair is supported. Multi-AZ DB clusters (the two-readable-standby topology) are not; Multi-AZ instances are.

Step 4: Alarm on replication lag

Replication lag is your recovery point objective. If the replica is 20 minutes behind when the region dies, you lose 20 minutes of writes. Alarm on it in the DR region:

TOPIC_ARN=$(aws sns create-topic --region us-west-2 \
  --name rds-dr-alerts --query TopicArn --output text)
aws sns subscribe --region us-west-2 --topic-arn "$TOPIC_ARN" \
  --protocol email --notification-endpoint you@example.com

aws cloudwatch put-metric-alarm --region us-west-2 \
  --alarm-name appdb-dr-replica-lag \
  --namespace AWS/RDS --metric-name ReplicaLag \
  --dimensions Name=DBInstanceIdentifier,Value=appdb-dr \
  --statistic Maximum --period 300 --evaluation-periods 3 \
  --threshold 300 --comparison-operator GreaterThanThreshold \
  --treat-missing-data breaching \
  --alarm-actions "$TOPIC_ARN"

This fires when lag stays above 300 seconds for 15 minutes; set the threshold to whatever RPO you've promised. --treat-missing-data breaching matters because a replica that stops reporting is usually a replica that stopped replicating. One gap: ReplicaLag reports -1 when replication isn't running at all, which won't trip a greater-than alarm, so add a second alarm with --comparison-operator LessThanThreshold --threshold 0 if you want that case covered.

Step 5: Write the failover script

Promotion is the failover. It severs replication, reboots the replica, and makes it a writable standalone instance. Save this as failover.sh:

#!/usr/bin/env bash
set -euo pipefail
REPLICA=appdb-dr
DR_REGION=us-west-2

status=$(aws rds describe-db-instances --region "$DR_REGION" \
  --db-instance-identifier "$REPLICA" \
  --query 'DBInstances[0].DBInstanceStatus' --output text)
if [ "$status" != "available" ]; then
  echo "Replica is '$status', promotion needs 'available'. Aborting." >&2
  exit 1
fi

aws rds promote-read-replica --region "$DR_REGION" \
  --db-instance-identifier "$REPLICA" \
  --backup-retention-period 7

aws rds wait db-instance-available --region "$DR_REGION" \
  --db-instance-identifier "$REPLICA"

echo "Promoted. New writer endpoint:"
aws rds describe-db-instances --region "$DR_REGION" \
  --db-instance-identifier "$REPLICA" \
  --query 'DBInstances[0].Endpoint.Address' --output text

Make it executable with chmod +x failover.sh. The status check exists because RDS refuses to promote a replica in the backing-up state.

Three runbook rules to write down next to this script. First, if the primary is still reachable (partial outage, planned failover), stop application writes and wait for ReplicaLag to hit 0 before promoting; replication is asynchronous, and anything unapplied at promotion time is gone. Second, promotion is one-way. Failing back means creating a new replica in the opposite direction and promoting again. Third, after promotion, repoint your application: update the connection secret, or better, front the database with your own Route 53 CNAME so failover is a DNS change instead of a redeploy.

You can wire the SNS topic to a Lambda that runs the promotion for hands-off failover, but gate it behind a human approval. Lag alarms fire for reasons short of a dead region, and promoting while the primary is still taking writes splits your data in two.

Verify it works

Check replication status in the DR region:

aws rds describe-db-instances --region us-west-2 \
  --db-instance-identifier appdb-dr \
  --query 'DBInstances[0].StatusInfos'

Expected output:

[
    {
        "StatusType": "read replication",
        "Normal": true,
        "Status": "replicating"
    }
]

Confirm backup replication is active. The source instance lists the replicated backup ARN, and the destination reports its status:

AB_ARN=$(aws rds describe-db-instances --region us-east-1 \
  --db-instance-identifier appdb-prod \
  --query 'DBInstances[0].DBInstanceAutomatedBackupsReplications[0].DBInstanceAutomatedBackupsArn' \
  --output text)
aws rds describe-db-instance-automated-backups --region us-west-2 \
  --db-instance-automated-backups-arn "$AB_ARN" \
  --query 'DBInstanceAutomatedBackups[0].Status' --output text

Expected output: replicating.

Finally, prove data flows. Insert a row on the primary, then query the replica's endpoint and check lag directly:

psql -h appdb-dr.xxxxxxxxxxxx.us-west-2.rds.amazonaws.com -U appuser -d appdb \
  -c "SELECT now() - pg_last_xact_replay_timestamp() AS lag;"

Expected output is a sub-second interval on an idle database:

      lag
-----------------
 00:00:00.412381

Run failover.sh once against a throwaway staging replica before you trust it. Success looks like the script printing the new endpoint and the instance's role in the console flipping from Replica to Instance.

Troubleshooting

An error occurred (InvalidDBInstanceState) when calling the CreateDBInstanceReadReplica operation: Automated backups are not enabled for this database instance. The source has BackupRetentionPeriod: 0. Fix: aws rds modify-db-instance --region us-east-1 --db-instance-identifier appdb-prod --backup-retention-period 7 --apply-immediately, wait for the first backup to finish, then retry.

An error occurred (InvalidDBInstanceState) when calling the PromoteReadReplica operation The replica is mid-backup or otherwise not available. Wait for it to return to available and rerun; the status check in failover.sh catches this before the API does. Schedule the replica's backup window away from your likely drill times.

ReplicaLag shows -1 or the alarm reports missing data. Replication isn't running. Check StatusInfos for an error status and pull recent events with aws rds describe-events --region us-west-2 --source-type db-instance --source-identifier appdb-dr. Move fast: RDS terminates PostgreSQL replication that stays broken for 30 consecutive days, after which you rebuild the replica from scratch.

start-db-instance-automated-backups-replication fails with an unsupported-region error. Either you ran it against the source region (it must run against the destination, with the source passed as an ARN), or the region pair doesn't support backup replication. aws rds describe-source-regions --region <destination> shows which sources are valid.

Next steps

  • Drill a cross-region point-in-time restore from the replicated backups with aws rds restore-db-instance-to-point-in-time --source-db-instance-automated-backups-arn .... That path saves you when the outage is a bad migration rather than a dead region.
  • If your RPO target is near zero and you can move engines, Amazon Aurora Global Database offers managed cross-region failover with typical replication lag under a second.
  • Add Route 53 health checks and DNS failover so promotion and traffic cutover happen in one motion.
  • Put a failover game day on the calendar. A runbook you've never executed is a guess; measure your real RTO and RPO quarterly.

Sources & further reading

  1. Creating a read replica in a different AWS Region — docs.aws.amazon.com
  2. Enabling cross-Region automated backups for Amazon RDS — docs.aws.amazon.com
  3. Promoting a read replica to be a standalone DB instance — docs.aws.amazon.com
  4. Monitoring read replication — docs.aws.amazon.com
  5. Finding information about replicated backups for Amazon RDS — docs.aws.amazon.com
  6. promote-read-replica - AWS CLI Command Reference — docs.aws.amazon.com
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.

Discussion 6

Join the discussion

Sign in or create an account to comment and vote.

Marco Bianchi @shipfast_marco · 1 day ago

looks solid but the real gotcha nobody mentions: read replicas cost full compute even when idle, and if you're doing cross-region that's doubled data transfer fees on top. for teams not running massive throughput, a cheaper play is just keeping automated backups replicated and promoting from snapshot when disaster hits — slower rto but way less bleed if you never actually need it.

Rashid Patel @rashid_patel · 1 day ago

one-command failover sounds nice until you realize the real pain is the DNS cutover and validating your app actually connects to the new endpoint. the script handles promotion fine, but i've seen teams get tripped up because their connection pooler cached the old region's IP and they ended up reading stale data for 10 minutes after failover anyway.

Paul Nguyen @pragmatic_paul · 1 day ago

@rashid_patel nailed it. promotion script solves the easy part. just add the dns and connection pool logic to your runbook and test it quarterly under load.

Brianna Cole @burned_out_bri · 19 hours ago

yeah this is the real gotcha nobody wants to talk about. the replica promotion is genuinely the easy part—it's the application layer that bites you. even with route53 failover policies, if your pooler doesn't respect ttl or you've got hardcoded ips anywhere (and you always do, in some weird config file), you're running a degraded system and don't know it. seen teams solve this by hard-coding the failover endpoint in their app at deploy time, but that defeats the whole 'transparent failover' thing.

Priya Nair @k8s_whisperer · 2 days ago

finally, a practical walkthrough that doesn't hand-wave the replication lag monitoring bit. bookmarking this.

Greg Tanaka @golang_greg · 2 days ago

100% on the lag monitoring—we had a replica lag spike during a region event last year that would've been invisible without the alarm, and it would've cost us consistency on failover. the tricky part nobody talks about is tuning the threshold; we ended up at 500ms but that took a few incidents to learn.

Related Reading