AWS Databases

RDS High Availability: Multi-AZ vs Read Replicas (and When to Use Each)

Two RDS features get confused more than any other pair on AWS, and the confusion costs real outages: Multi-AZ and read replicas. They both make a second copy of your database in another place, so at a glance they look like the same idea with two names. They are not. Multi-AZ is for high availability — a synchronous standby that exists only to take over when the primary dies. Read replicas are for read scaling — asynchronous copies that serve read traffic and can lag the primary by seconds. One protects you from failure; the other spreads load. Reach for the wrong one and you either build “HA” that silently serves stale data during a failover, or you scale reads onto a standby that refuses every connection.

The trap is that the words replica and standby both mean “another copy,” and marketing slides show both as a box with an arrow. But the arrow is the whole story: a synchronous arrow (commit isn’t done until the standby has it) buys you zero-data-loss failover and nothing else — you can’t read from that standby. An asynchronous arrow (commit returns immediately, the copy catches up later) buys you extra readable capacity and a migration/DR landing pad — but it lags, and it will never fail over on its own. If you remember only one sentence from this article, make it this: Multi-AZ answers “what happens when the primary fails”; read replicas answer “where do my extra reads go” — and neither answers the other’s question.

By the end you will place each mechanism precisely: the classic Multi-AZ instance (one hidden synchronous standby, 60–120 s failover by flipping the endpoint’s DNS record) versus the newer Multi-AZ DB cluster (one writer plus two readable standbys, semi-synchronous, ~35 s failover, more write throughput); read replicas (asynchronous, up to 15, cross-Region, cascading, with replica lag you must watch and a one-way promotion to standalone); what a client actually experiences during a failover and why connection-pool and retry logic decide whether users notice; the reader endpoint concept; and where Aurora changes the rules with shared storage. Because you will return to this mid-design and mid-incident, every dimension — settings, triggers, limits, metrics, failure modes — is laid out as a scannable table, with aws CLI and Terraform for every operation, a force-failover lab, and a symptom→confirm→fix playbook.

What problem this solves

Databases fail in two directions, and teams routinely defend against one while calling it the other. The first direction is availability: the primary instance’s host dies, its Availability Zone loses power, or a patch reboots it — and every write path in your application stops. The second direction is read capacity: the primary is healthy but drowning, because reporting queries, dashboards, search, and read-heavy API traffic all pile onto the one instance that also has to take writes. These are different failures with different fixes, and the single most expensive mistake in RDS operations is treating them as one.

What breaks without the distinction is subtle and usually discovered during an incident. A team enables Multi-AZ, sees “we have a replica now,” and points their reporting service at the standby — except the Multi-AZ standby accepts no connections, so reporting either fails outright or (worse) they build read scaling on a read replica and then assume that replica will fail over automatically when the primary dies. It won’t. During the real outage the primary is gone, the “backup” replica keeps happily serving reads (stale, and now the only writable path is down), and nobody promoted anything because nobody wired promotion. The reverse failure is just as common: a team adds five read replicas to fix a write bottleneck, discovers replicas do nothing for writes, and burns weeks and rupees before someone says the quiet part — replicas scale reads, not writes.

Who hits this: every team standing up their first production RDS database; every application that succeeds (success is exactly when read traffic outgrows one instance); every architect drafting a DR plan who has to answer “what is our RTO and RPO for the database, precisely?” The fix is not a feature — it is a clear mental model of which mechanism answers which question, applied before the incident. Here is that model in one glance, the table you scan first:

Question you’re actually asking Wrong tool people reach for Right tool Why
“What happens when the primary instance/AZ dies?” A read replica (“we’ll fail over to it”) Multi-AZ (instance or cluster) Automatic failover; synchronous, zero data loss
“Where do my extra read/report queries go?” The Multi-AZ standby (“it’s just sitting there”) Read replicas (or Multi-AZ cluster readers) Readable capacity; the classic standby is not readable
“How do I not lose committed data on failure?” Async read replica Multi-AZ (synchronous) Sync commit means the standby has every write
“How do I scale writes?” More read replicas Neither — bigger instance / Aurora / sharding Replicas and standbys don’t add write capacity
“How do I serve users in another Region?” Multi-AZ (it’s same-Region only) Cross-Region read replica Multi-AZ never spans Regions; replicas can
“How do I cut over to a new database / migrate?” Multi-AZ failover Promote a read replica Promotion makes a replica an independent primary
“How do I take backups without hurting the primary?” Run backups off-hours and hope Multi-AZ (backups come from the standby) Standby absorbs the backup I/O, primary doesn’t stall
“How do I get sub-30 s failover and read scaling together?” Multi-AZ instance + separate replicas Multi-AZ DB cluster or Aurora Readable standbys + faster failover in one topology

Read that table top to bottom and the two mechanisms stop blurring. The rest of this article is the depth behind each row.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should be comfortable creating an RDS instance and connecting to it — if not, start with Launch RDS for MySQL and PostgreSQL, Hands-On, which stands up the single-AZ instance this article makes highly available. You need working knowledge of a VPC with private subnets across at least two Availability Zones (Multi-AZ needs a DB subnet group spanning AZs), security groups, and the difference between the Console, the aws CLI and Terraform. Basic SQL and the idea of a connection pool help, because half of “why didn’t failover work” turns out to be client-side.

This sits in the Databases / reliability track. It is the availability-and-scale layer directly on top of the store-selection decision in AWS Databases: RDS, DynamoDB and Aurora — Choose the Right Store, and it pairs tightly with connection handling from RDS Connection Timeouts: A Troubleshooting Playbook (failover is a connection event) and with the multi-Region patterns in AWS Multi-Region Active-Active Architecture (where cross-Region replicas and promotion become the cutover mechanism). When RDS HA stops being enough, Aurora Serverless v2: Setup and Scaling is the graduation path.

A quick map of who owns each layer, so during an incident you pull in the right person instead of blaming “the database”:

Layer What lives here Usually owned by What it decides / can break
Application / client Connection pool, retry logic, DNS caching, endpoint config App / dev team Whether failover is invisible or a 5-minute outage
RDS control plane Multi-AZ toggle, replica creation, promotion, failover DBA / platform team RTO/RPO; read routing; who is primary
Replication Sync (Multi-AZ) vs async (replicas), lag AWS (managed) + you Data loss on failover; read staleness
Storage EBS gp3/io2 per instance; local NVMe (Multi-AZ cluster) AWS (managed) Failover speed; write throughput; IOPS
Network VPC, DB subnet group (multi-AZ), security groups, DNS Network team Reachability; whether the standby’s AZ is usable
Observability CloudWatch metrics, RDS events, SNS subscriptions Platform / SRE Whether you know a failover happened
Cost / FinOps 2× (Multi-AZ), per-replica, cross-Region transfer FinOps + owners The bill; over-replication; egress surprises

Core concepts

Six ideas make every later decision obvious. Internalise these and the option tables read themselves.

Availability and durability are not scalability. Availability is the probability the database is reachable and writable right now; durability is the guarantee a committed write survives a failure; scalability is how much load it can carry. Multi-AZ raises availability and durability (a synchronous standby with automatic failover). Read replicas raise read scalability (extra readable copies). Confusing these three is the root of every mistake in this topic — you cannot buy scalability with an HA feature or availability with a scaling feature.

Synchronous vs asynchronous replication decides everything downstream. In synchronous replication (Multi-AZ instance), the primary does not acknowledge a commit until the standby has durably written it — so the standby is always current (zero data loss, RPO ≈ 0), but the standby can’t also be serving reads without risking consistency, and there’s a small write-latency tax. In asynchronous replication (read replicas), the primary commits immediately and ships the change to replicas afterward — so replicas are readable and cheap to add, but they lag and a failure can lose the un-shipped tail (nonzero RPO). Sync = safety; async = scale. That single axis explains why one is HA and the other is read scaling.

A standby is not a replica, and a replica is not a standby. The Multi-AZ standby is invisible: no endpoint, no reads, no separate identity — it’s a shadow that becomes the primary on failover. A read replica is a first-class DB instance with its own endpoint, its own metrics, and its own life — you can read from it, give it its own replicas, put it in another Region, and promote it. They are different objects with different jobs that happen to both involve “a second copy.”

RTO and RPO are the numbers HA is measured in. RTO (Recovery Time Objective) is how long you can be down; RPO (Recovery Point Objective) is how much data you can lose. Multi-AZ gives RPO ≈ 0 (sync) and RTO of ~60–120 s (instance) or ~35 s (cluster). A promoted read replica gives RPO = “however far it had lagged” and RTO = “however long promotion + repointing takes.” When someone asks “are we HA?”, they are really asking “what are our RTO and RPO?” — answer in those terms.

Failover is a DNS event, not a data-copy event. When a Multi-AZ primary fails, RDS promotes the standby and updates the DNS record behind your unchanged endpoint to point at the new primary. Your connection string never changes; the IP it resolves to does. This is why failover is fast (no data to move — the standby already had it) and why the client side matters so much (if your client cached the old IP, it keeps talking to a dead host).

Read scaling has a routing problem the standby doesn’t. A Multi-AZ standby needs no routing — you never talk to it until it is the primary. Read replicas need you to decide which traffic goes where: writes and read-your-own-write to the primary, bulk reads to replicas. Classic RDS read replicas each have their own endpoint and no managed load balancer, so you route in the app, via a proxy, or (in a Multi-AZ cluster / Aurora) via a reader endpoint. Getting routing wrong is how “we have replicas” still overloads the writer.

The vocabulary in one table

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

Term One-line definition Which mechanism Why it matters
Multi-AZ instance One primary + one hidden synchronous standby in another AZ HA Automatic failover; standby not readable
Multi-AZ DB cluster One writer + two readable standbys across 3 AZs, semi-sync HA + some read Faster failover; adds read capacity
Standby The passive copy that becomes primary on failover HA No endpoint, no reads until promoted
Read replica An asynchronous, readable copy that is its own instance Read scaling Lags; never auto-fails-over
Synchronous replication Commit waits for the standby to have the write Multi-AZ RPO ≈ 0; small write-latency tax
Asynchronous replication Commit returns immediately; copy catches up later Read replica Readable but lagging; nonzero RPO
Failover Promoting the standby and repointing the endpoint DNS Multi-AZ The availability event; ~35–120 s
Promotion Turning a read replica into an independent primary Read replica One-way; used for cutover/DR
ReplicaLag Seconds a replica is behind the source Read replica The staleness metric to alarm on
Endpoint (instance) DNS name for one instance; CNAME flips on failover Both What your app connects to
Reader endpoint Load-balanced DNS across readable standbys/replicas Multi-AZ cluster / Aurora Where bulk reads should go
RTO / RPO Recovery time / recovery point objectives HA planning How HA is actually measured
DB subnet group The set of subnets (across AZs) RDS can place instances in Both Multi-AZ needs ≥2 AZs here

The distinction, made concrete

Because this is the point, here it is enumerated across every dimension that differs. Scan the column that matches what you’re trying to achieve:

Dimension Multi-AZ (HA) Read replica (read scaling)
Primary purpose Availability + durability Read throughput / offload
Replication Synchronous (instance) / semi-sync (cluster) Asynchronous
Data loss on failure (RPO) ≈ 0 (committed writes are safe) Whatever hadn’t shipped yet (> 0)
Standby/replica readable? Instance: no; Cluster: yes (2 readers) Yes (that’s the point)
Automatic failover? Yes (RDS promotes + repoints DNS) No (you must manually promote)
Recovery time (RTO) ~60–120 s (instance) / ~35 s (cluster) Minutes (promote + repoint app)
Same or cross-Region? Same Region only Same or cross-Region
How many? 1 standby (instance) / 2 (cluster) Up to 15 per source
Endpoint behaviour One endpoint; CNAME flips on failover Each replica has its own endpoint
Adds write capacity? No No
Cost shape ~2× (instance) / ~3× (cluster) Per-replica instance (+ egress if cross-Region)
You often want… This on every production DB This in addition, when reads outgrow one box

The clarifying myth-buster — what each explicitly does not do, because these assumptions cause outages:

The tempting assumption Reality Consequence if believed
“Multi-AZ gives me a second instance to read from” The instance standby is not readable Reporting service fails to connect; you’re surprised at 2 a.m.
“My read replica will fail over automatically” Replicas never auto-fail-over The primary dies and nobody promotes; write path is down
“Read replicas fix my write bottleneck” Replicas add read capacity only Weeks wasted; writes still bottlenecked
“Multi-AZ protects another Region” Multi-AZ is same-Region only A Region event takes you fully down
“Failover changes my connection string” The endpoint name is stable; only the IP flips Ops scrambles to update configs that didn’t need changing
“Promotion is reversible” Promotion is one-way You can’t re-attach; you rebuilt the wrong way
“A replica has zero lag because it’s on AWS” Async replication lags, sometimes a lot Users see stale data; read-your-own-write breaks

Multi-AZ instance deployment, option by option

The classic Multi-AZ deployment is one primary DB instance with one synchronous standby in a different Availability Zone of the same Region. AWS provisions, patches and monitors both; you interact only with the primary’s endpoint. The standby is a true shadow: it has no endpoint, accepts no connections, and its only job is to be ready to become the primary.

What Multi-AZ actually gives you

Enumerate the guarantees so you know exactly what you bought (and didn’t):

Property Multi-AZ instance behaviour What it does not give
Standby location Different AZ, same Region No cross-Region protection
Replication Synchronous, block-level (physical) Not logical/engine replication
Data loss (RPO) ≈ 0 — committed writes are on both
Readability of standby None — zero reads No read offload
Failover Automatic, ~60–120 s, DNS CNAME flip Not instant; not sub-second
Backups Taken from the standby (no primary I/O stall)
Patching Standby patched first, then failover, then old primary Brief failover blip during patching
Endpoint One; unchanged across failover You still need client reconnect logic
Write latency Slightly higher (waits for standby ack) Not free — sync has a tax
Cost ~2× a single-AZ instance The standby is billed but idle

Enabling and configuring it

Turning on Multi-AZ is a single flag, but it’s an online modification that itself provisions the standby and syncs it (which takes time and, briefly, adds load). The knobs worth knowing:

Setting Values Default When to change Trade-off / gotcha
multi_az / --multi-az true / false false Every production DB → true Enabling syncs a full copy first (takes minutes–hours)
DB subnet group ≥ 2 subnets in ≥ 2 AZs required Must span AZs for Multi-AZ Single-AZ subnet group blocks Multi-AZ
apply_immediately true / false false Convert now vs in maintenance window false defers the change to the window
Backup retention 1–35 days (0 = off) 7 Enable PITR; needed for replicas too 0 disables automated backups and replicas
Maintenance window Weekly time slot AWS-assigned Align patch-driven failovers to low traffic Patches can trigger a failover blip
Instance class Both nodes same class Size for peak write load Standby matches primary (2× cost)
Storage type gp3 / io2 gp3 IOPS-heavy → provision more Both nodes get the same storage

Convert an existing single-AZ instance to Multi-AZ with one modify call:

# Convert a running single-AZ instance to Multi-AZ (online; standby syncs first)
aws rds modify-db-instance \
  --db-instance-identifier orders-prod \
  --multi-az \
  --apply-immediately
# Watch it go from single-AZ to multi-az:
aws rds describe-db-instances --db-instance-identifier orders-prod \
  --query "DBInstances[0].{MultiAZ:MultiAZ,Status:DBInstanceStatus,AZ:AvailabilityZone,Standby:SecondaryAvailabilityZone}"
# Terraform: a Multi-AZ instance from the start
resource "aws_db_instance" "orders" {
  identifier             = "orders-prod"
  engine                 = "postgres"
  engine_version         = "16.4"
  instance_class         = "db.r6g.large"
  allocated_storage      = 100
  max_allocated_storage  = 500
  storage_type           = "gp3"
  multi_az               = true            # <-- the HA switch
  db_subnet_group_name   = aws_db_subnet_group.private.name  # must span AZs
  vpc_security_group_ids = [aws_security_group.db.id]
  backup_retention_period = 14             # >0 also required for read replicas
  storage_encrypted      = true
  deletion_protection    = true
  publicly_accessible    = false
}

What happens during a failover, second by second

Understanding the sequence tells you exactly where client logic has to catch the fall:

Phase Roughly when What AWS does What the client sees
Detection 0–30 s Health checks confirm the primary is unreachable Queries hang / error; connections stall
Promotion ~starts immediately after Standby is promoted to primary (it already has all data) Still no writable DB
DNS update ~mid-failover Endpoint’s DNS record repointed to the new primary Old connections are dead; new lookups get new IP
Recovery until ~60–120 s New primary finishes crash recovery, opens for connections App that re-resolves + reconnects succeeds
New standby minutes after A fresh standby is built in another AZ Transparent; HA restored

The lesson embedded in that table: AWS gets you a writable primary in ~60–120 s, but the client only recovers when it re-resolves DNS and reconnects. If your client caches the dead IP or your pool never retries, AWS’s fast failover is invisible to you and the “outage” lasts as long as your DNS cache or timeout — which can be far longer than the failover itself.

Failover triggers

A failover is not only the dramatic AZ outage — several routine events cause one, which is exactly why your app must always be failover-ready:

Trigger Category Automatic? Notes
Availability Zone outage / power loss Unplanned Yes The canonical case Multi-AZ defends
Primary host / hardware failure Unplanned Yes AWS detects and promotes
Storage (EBS) failure on primary Unplanned Yes Standby has its own storage copy
Loss of network to the primary Unplanned Yes Health check driven
OS / engine patching Planned Yes (blip) Standby patched first, then failover
Instance-class modification Planned Yes (blip) Resize applies via failover on Multi-AZ
Manual reboot --force-failover Planned Yes The way you test failover (see the lab)
DB instance reboot (no force) Planned No Reboots in place; no AZ flip

Multi-AZ DB cluster — the newer, faster, readable topology

The classic Multi-AZ instance has two limitations: the standby is wasted (no reads) and failover is ~60–120 s. The Multi-AZ DB cluster deployment addresses both. It runs one writer and two readable standby replicas across three AZs, replicates semi-synchronously (the writer acknowledges a commit once at least one of the two readers confirms it — it doesn’t wait for both), and fails over in ~35 seconds. Because two nodes are readable, you also get a reader endpoint for read offload, and the write path is faster (local NVMe storage plus the one-of-two ack).

Instance vs cluster, side by side

The decision table — most teams should know both exist and pick deliberately:

Dimension Multi-AZ instance Multi-AZ DB cluster
Topology 1 primary + 1 standby 1 writer + 2 readable standbys
AZs spanned 2 3
Replication Synchronous (1 standby) Semi-synchronous (ack from 1 of 2 readers)
Standbys readable? No Yes (2 readers, via reader endpoint)
Failover time ~60–120 s ~35 s
Write throughput Baseline Higher (local NVMe + 1-of-2 ack)
Read offload None Two reader nodes
Endpoints One instance endpoint Cluster (writer) + reader endpoint
Storage EBS gp3/io2 Local NVMe SSD (specific classes)
Engines All RDS engines MySQL 8.0.28+, PostgreSQL 13.4+ (Provisioned)
Instance classes Broad NVMe classes (e.g. db.m6gd, db.r6gd, db.m5d, db.r5d)
Cost ~2× ~3× (three nodes)
Pick it when Any production DB needing HA HA plus faster failover / some read scaling

Endpoints and support caveats

The cluster’s readable standbys come with a managed reader endpoint — the thing classic replicas lack — but there are real support boundaries to check before you commit:

Aspect Multi-AZ DB cluster detail Gotcha
Writer endpoint ...cluster-xxxx... — all writes + read-your-own-write Sending reads here wastes writer capacity
Reader endpoint ...cluster-ro-xxxx... — load-balances the 2 readers Reads may be slightly behind the writer
Reader consistency Semi-sync; can lag under heavy write Not for strict read-after-write
Engine MySQL 8.0.28+, PostgreSQL 13.4+/14+/15+/16+ MariaDB / Oracle / SQL Server: not supported
Mode Provisioned only No Serverless; no burstable t classes
Read replicas from a cluster Limited/engine-dependent Don’t assume classic replica features apply
Converting instance ↔ cluster Not a simple toggle Plan a proper migration path
# Create a Multi-AZ DB cluster (PostgreSQL) — 1 writer + 2 readable standbys
aws rds create-db-cluster \
  --db-cluster-identifier orders-mazc \
  --engine postgres --engine-version 16.4 \
  --db-cluster-instance-class db.r6gd.large \
  --allocated-storage 100 --storage-type io1 --iops 3000 \
  --master-username appadmin --manage-master-user-password \
  --db-subnet-group-name db-private --vpc-security-group-ids sg-0abc123
# Two endpoints appear: the cluster (writer) endpoint and the -ro reader endpoint
aws rds describe-db-clusters --db-cluster-identifier orders-mazc \
  --query "DBClusters[0].{Writer:Endpoint,Reader:ReaderEndpoint,Status:Status}"
# Terraform: Multi-AZ DB cluster (note aws_rds_cluster with db_cluster_instance_class)
resource "aws_rds_cluster" "orders_mazc" {
  cluster_identifier        = "orders-mazc"
  engine                    = "postgres"
  engine_version            = "16.4"
  db_cluster_instance_class = "db.r6gd.large"   # NVMe class → Multi-AZ cluster
  allocated_storage         = 100
  storage_type              = "io1"
  iops                      = 3000
  master_username           = "appadmin"
  manage_master_user_password = true
  db_subnet_group_name      = aws_db_subnet_group.private.name
  vpc_security_group_ids    = [aws_security_group.db.id]
  storage_encrypted         = true
  deletion_protection       = true
}

Read replicas — asynchronous copies for read scaling

A read replica is a separate, readable DB instance kept up to date from a source database via the engine’s native asynchronous replication (MySQL/MariaDB binlog, PostgreSQL streaming replication, Oracle/SQL Server equivalents). Because replication is asynchronous, the replica can serve reads immediately — but it lags the source, and it is never an automatic failover target. It is the tool for read scaling, reporting isolation, near-Region reads, and (via promotion) migrations and DR.

What read replicas give you

Property Read replica behaviour Limit / caveat
Replication Asynchronous (engine-native) Lag is inherent; watch ReplicaLag
Readable Yes — full read traffic Read-only until promoted
Count Up to 15 per source (MySQL/MariaDB/PostgreSQL/Oracle) SQL Server: up to 5, edition-dependent
Region Same or cross-Region Cross-Region adds transfer cost + lag
Cascading Replica of a replica (MySQL/MariaDB/PostgreSQL) Spreads replication load off the source
Own Multi-AZ A replica can itself be Multi-AZ Good for a replica you’ll promote for DR
Own endpoint Each replica has its own DNS endpoint No managed reader endpoint (unlike Aurora)
Promotion Become a standalone read/write primary One-way; breaks replication
Backups Can have their own backup retention Needed if you’ll promote it
Prereq Source must have backup_retention_period > 0 0 blocks replica creation

Where read replicas earn their place

Four distinct jobs, each a legitimate reason to add a replica:

Use case How the replica helps Watch-out
Read scaling Offload read-heavy API traffic from the writer Route only lag-tolerant reads there
Reporting / analytics isolation Heavy BI queries don’t hurt the OLTP primary Long queries can increase lag
Near-Region reads Cross-Region replica serves local users fast Egress cost; higher lag
Migration / cutover Promote to a new independent primary One-way; plan the switch carefully
Disaster recovery (warm) Cross-Region replica you can promote on a Region event RPO = the lag at promotion time
Engine/version testing Point test workloads at a replica Still read-only pre-promotion

Per-engine limits and topology

The numbers differ by engine — don’t assume “15 everywhere”:

Engine Max read replicas Cross-Region? Cascading? Notes
MySQL 15 Yes Yes Binlog-based; cascading spreads load
MariaDB 15 Yes Yes Similar to MySQL
PostgreSQL 15 Yes Yes (cascading) Streaming replication; slots to watch
Oracle Up to 5 (edition/feature-dependent) Yes (with options) Limited Active Data Guard concepts
SQL Server Up to 5 Limited No Edition-dependent; more restrictions
# Create an in-Region read replica (source must have backups enabled)
aws rds create-db-instance-read-replica \
  --db-instance-identifier orders-replica-1 \
  --source-db-instance-identifier orders-prod \
  --db-instance-class db.r6g.large

# Create a CROSS-REGION read replica (note the full source ARN + target region)
aws rds create-db-instance-read-replica \
  --db-instance-identifier orders-replica-blr \
  --source-db-instance-identifier arn:aws:rds:ap-south-1:111122223333:db:orders-prod \
  --region ap-south-2 \
  --db-instance-class db.r6g.large
# Terraform: an in-Region read replica is just replicate_source_db
resource "aws_db_instance" "orders_replica_1" {
  identifier          = "orders-replica-1"
  replicate_source_db = aws_db_instance.orders.identifier  # <-- makes it a replica
  instance_class      = "db.r6g.large"
  # No engine/username/storage here — inherited from the source
  publicly_accessible = false
  skip_final_snapshot = true
}

Replica lag — the metric that defines a replica’s health

Because replication is asynchronous, ReplicaLag (seconds behind the source) is the single most important replica metric. Zero is ideal; a steadily climbing value means the replica can’t keep up. The causes, ranked by how often they bite:

Cause of lag Why it happens How to confirm Fix
Write burst on the source Replica applies changes serially; can’t keep up with a spike ReplicaLag spikes with WriteIOPS on source Size replica up; smooth writes; accept transient lag
Undersized replica Replica weaker than source, can’t apply fast enough Replica CPU/IO pinned while source is fine Match/exceed source class on the replica
Long-running transaction / lock A big transaction blocks apply on the replica PG: pg_stat_activity; MySQL: SHOW PROCESSLIST Break up transactions; avoid huge single commits
Heavy read query on the replica Reporting query blocks replication apply Lag rises during report runs Isolate reporting; tune queries; separate replica
Bulk DDL / index build Schema change replays slowly on the replica Lag jumps at deploy time Schedule DDL off-peak; expect a lag window
Single-threaded apply Engine applies changes on limited threads Lag under high concurrent write Parallel replication settings where supported
Network (cross-Region) Distance + transfer for cross-Region replicas Cross-Region replica lags more than in-Region Expect it; size for it; keep DR reads tolerant
# Watch ReplicaLag on a replica (should trend toward 0)
aws cloudwatch get-metric-statistics --namespace AWS/RDS \
  --metric-name ReplicaLag \
  --dimensions Name=DBInstanceIdentifier,Value=orders-replica-1 \
  --start-time $(date -u -d '1 hour ago' +%FT%TZ 2>/dev/null || date -u -v-1H +%FT%TZ) \
  --end-time $(date -u +%FT%TZ) --period 60 --statistics Maximum Average

Promotion — the one-way door

Promoting a read replica detaches it from its source and makes it a standalone, writable primary. Replication stops permanently; there is no “demote” to re-attach. Promotion is how you cut over a migration, break off a shard, or activate a DR replica after a Region failure. What actually changes:

Aspect Before promotion After promotion Irreversible?
Role Read-only replica of a source Independent read/write primary Yes — can’t re-attach
Replication Applying changes from source Stopped forever Yes
Writes Rejected Accepted
Endpoint Its own replica endpoint Same endpoint, now writable App must repoint here
Backups Optional Its own automated backups begin
Multi-AZ Optional on the replica Carries over if it was set
Data currency As current as its lag allowed Frozen at promotion instant (+ its lag) RPO = lag at promote
# Promote a read replica to a standalone primary (ONE-WAY — no going back)
aws rds promote-read-replica \
  --db-instance-identifier orders-replica-1 \
  --backup-retention-period 7
# It briefly reboots, then accepts writes. Repoint the app to its endpoint.

The critical operational point: because promotion captures whatever lag existed at that instant, a DR promotion after a source failure has an RPO equal to the un-replicated tail. If the replica lagged 12 seconds when the source vanished, you lost ~12 seconds of writes. This is the fundamental difference from Multi-AZ (RPO ≈ 0) — and the reason read replicas are DR, not HA.

Failover and the client experience

The most-skipped truth in RDS HA is that failover is only half done by AWS. AWS promotes the standby and repoints DNS; your application has to notice the connection died, re-resolve the endpoint, and reconnect. If it doesn’t, the ~60–120 s failover becomes a multi-minute outage on your side. Map the triggers to what the client actually experiences:

Trigger What AWS does What the client experiences What the client must do
AZ outage Promote standby, repoint DNS In-flight queries error; connections drop Reconnect + retry
Host failure Same Same Same
Patching (planned) Failover during window Brief connection drop Reconnect (ideally off-peak)
Instance resize Applies via failover Brief drop Reconnect
reboot --force-failover Immediate failover Connections drop within seconds Reconnect (this is your test)
Replica promotion Replica becomes primary Old writes were going nowhere (writer gone) Repoint to the promoted endpoint

Why the client side decides your real RTO

Enumerate the client-side settings that turn a fast AWS failover into an invisible event — or a long one:

Client concern Bad default that bites What to set Why
DNS caching (JVM) Java can cache DNS forever (with a SecurityManager) or 30 s networkaddress.cache.ttl=5 (or lower) So it re-resolves to the new primary fast
DNS caching (OS/app) Long TTL caches / no re-resolve Honour RDS’s ~5 s TTL; don’t pin IPs Same reason
Connection pool validation Hands out dead connections after failover Test-on-borrow / validation query Detects the severed connection
Pool max age / idle reap Keeps stale connections indefinitely Bound connection lifetime Forces periodic re-resolve/reconnect
Connect/socket timeout 30–60 s+ makes failover feel like an outage Short connect timeout + retry Fail fast, then reconnect
Retry logic No retry → first errors surface to users Retry transient errors with backoff Rides over the ~1–2 min window
Statement idempotency Blind retries double-apply writes Idempotent writes / transactional retries Safe to retry

The single highest-value change for JVM apps is capping DNS TTL — the classic “database failed over in 90 seconds but our app was down for 15 minutes” incident is almost always a JVM caching the dead primary’s IP. For the deeper connection-recovery playbook (pools, timeouts, FATAL: the database system is starting up), see RDS Connection Timeouts: A Troubleshooting Playbook.

Failover/recovery time, three topologies compared

Topology Typical recovery Data loss (RPO) Automatic? Read scaling included?
Multi-AZ instance ~60–120 s ≈ 0 Yes No
Multi-AZ DB cluster ~35 s ≈ 0 (semi-sync) Yes Yes (2 readers)
Read replica promotion Minutes (manual) = lag at promote No (It was the read layer)
Aurora Often < 30 s (frequently ~15 s) ≈ 0 (shared storage) Yes Yes (up to 15 readers)

Endpoints — connect to the right one

Half of “our replicas don’t help” is traffic hitting the wrong endpoint. Know exactly what each endpoint routes to:

Endpoint Exists on Routes to Use for On failover
DB instance endpoint Single-AZ & Multi-AZ instance The primary instance All traffic (writes + reads) Same name; DNS flips to new primary
Cluster (writer) endpoint Multi-AZ DB cluster / Aurora Current writer Writes + read-your-own-write Auto-points to new writer
Reader endpoint Multi-AZ DB cluster / Aurora Load-balanced readers Bulk / lag-tolerant reads Drops failed readers, balances rest
Replica endpoint Each RDS read replica That one replica Reads directed at that replica Doesn’t move (it’s not a failover target)
Custom endpoint Aurora only A chosen subset Isolate workloads (e.g. analytics) You define membership

The takeaways: on a classic Multi-AZ instance you have exactly one endpoint and it always points at the primary — simple. With classic read replicas, there is no managed reader endpoint; each replica has its own DNS name and you distribute reads (in app config, via a proxy like RDS Proxy, or a Route 53 weighted record). Only the Multi-AZ cluster and Aurora give you a managed reader endpoint. Assuming an “RDS reader endpoint” exists for classic replicas is a common and costly design error.

Monitoring HA — watch the signals that predict trouble

You cannot manage what you don’t watch, and HA failures announce themselves in metrics before users notice. The CloudWatch metrics that matter for Multi-AZ and replicas:

Metric Namespace What it tells you Alarm starting point
ReplicaLag AWS/RDS Seconds a replica is behind the source > 30 s sustained
DatabaseConnections AWS/RDS Connection count vs max_connections > 80% of max
CPUUtilization AWS/RDS Instance saturation (primary or replica) > 80% sustained
FreeableMemory AWS/RDS Memory pressure < 10% of RAM
ReadIOPS / WriteIOPS AWS/RDS I/O demand; write bursts drive lag Trend vs provisioned
DiskQueueDepth AWS/RDS I/O saturation > 5 sustained
FreeStorageSpace AWS/RDS Storage exhaustion → read-only < 10% free
SwapUsage AWS/RDS Memory over-commit Rising from 0
OldestReplicationSlotLag (PG) AWS/RDS Replication slot backlog Rising
TransactionLogsDiskUsage (PG) AWS/RDS WAL piling up (a stuck replica) Rising
ReplicationSlotDiskUsage (PG) AWS/RDS Slot retaining WAL for a lagging replica Rising
MaximumUsedTransactionIDs (PG) AWS/RDS Wraparound risk (vacuum/replica health) Approaching threshold

Metrics tell you a state; RDS events tell you a transition — and a failover is a transition. Subscribe an SNS topic to the RDS event categories so a failover pages you rather than being discovered in the metrics later:

Event category Example message Meaning Act on it
failover “Multi-AZ instance failover started” Failover is underway Expect connection drops; confirm app reconnected
failover “Multi-AZ instance failover completed” New primary is serving Verify write path healthy
availability “DB instance restarted” Reboot/recovery occurred Correlate with a trigger
failure “DB instance failed / recovery” Instance-level failure Investigate root cause
maintenance “Patch applied (may cause failover)” Planned patch/failover Schedule for low traffic
read replica “Replica has fallen behind the source” Lag threshold exceeded Investigate lag causes
# Subscribe to RDS failover/availability events for a specific instance
aws rds create-event-subscription \
  --subscription-name rds-ha-events \
  --sns-topic-arn arn:aws:sns:ap-south-1:111122223333:rds-alerts \
  --source-type db-instance \
  --event-categories '["failover","availability","failure","maintenance"]' \
  --source-ids orders-prod

Backups from the standby — a quiet Multi-AZ win

One benefit of Multi-AZ that has nothing to do with failover: automated backups and snapshots are taken from the standby, not the primary. On a single-AZ instance, the backup can briefly suspend I/O or elevate latency on the one instance serving your app. On Multi-AZ, that I/O hits the idle standby instead, so the primary keeps serving without a backup-window hiccup. It’s a performance and durability bonus that rides along with the availability you were buying anyway:

Aspect Single-AZ Multi-AZ
Backup source The primary (your live instance) The standby
I/O impact of backup Possible brief suspension / latency None on the primary
Snapshot impact Same instance under load Offloaded to standby
Durability One AZ Two AZs (sync)
PITR Yes (retention > 0) Yes (retention > 0)
Recommended retention 7–35 days 7–35 days

When RDS HA isn’t enough — the Aurora contrast

RDS Multi-AZ and read replicas are the right answer for most relational workloads. But their architecture — instance-attached storage, physical/async replication — sets ceilings that Amazon Aurora removes by decoupling compute from a shared, distributed storage layer (six copies across three AZs). Because every Aurora replica reads the same storage as the writer, replicas double as read scaling and failover targets, failover is faster (no data to copy), reader lag is sub-100 ms, and you can auto-scale readers. The trade is Aurora’s compatibility envelope and cost model. Side by side:

Dimension RDS Multi-AZ + read replicas Aurora
Storage Per-instance EBS (or local NVMe for cluster) Shared distributed volume, 6 copies / 3 AZs
Replica = failover target? No (replicas) / standby only (Multi-AZ) Yes — up to 15 replicas are both
Failover time ~60–120 s (instance) / ~35 s (cluster) Often < 30 s (frequently ~15 s)
Reader lag Seconds (async replicas) Typically < 100 ms (shared storage)
Managed reader endpoint Cluster/Aurora only (not classic replicas) Yes
Replica auto-scaling No Yes (add/remove readers on load)
Max readers 15 read replicas 15 Aurora Replicas
Storage ceiling 64 TiB (engine-dependent) 128 TiB, auto-grow
Cross-Region Cross-Region read replica Aurora Global Database (< 1 s)
Engine parity 100% vanilla engine MySQL/PostgreSQL-compatible (most, not all)
Cost model Per-instance Per-instance or ACU (Serverless v2) + I/O
Pick it when Standard relational HA + read scaling Faster failover, low-lag reads, auto-scaling readers

If your requirements are “sub-30 s failover, near-zero reader lag, readers that also fail over, and auto-scaling read capacity,” you have described Aurora — see AWS Databases: RDS, DynamoDB and Aurora — Choose the Right Store for the full decision and Aurora Serverless v2: Setup and Scaling for the auto-scaling compute path.

Architecture at a glance

The diagram traces one database’s reliability topology left to right and marks where each mechanism helps and where each one bites. Start at the left: the application connects through a single DB endpoint — a DNS CNAME whose name never changes. That endpoint points at the primary in AZ-a, which takes all writes. Two very different copies branch from the primary. Upward, a synchronous standby in AZ-b receives every commit before it’s acknowledged (badge 3) — it is the HA path: zero data loss, no reads, and the target the endpoint’s DNS flips to on failover (badge 1). Alongside it, backups are taken from that standby (badge 4), sparing the primary the backup I/O stall. Downward and to the right, read replicas receive changes asynchronously (badge 5) — they are readable and scale reads, but they lag and never fail over automatically; a cross-Region replica (badge 6) adds local reads and a promotable DR landing pad at the cost of egress and more lag. The primary node carries badge 2, the failover triggers that flip it — AZ outage, host failure, patching, or a manual reboot --force-failover.

Notice the two arrows leaving the primary are the whole lesson: the synchronous arrow (purple) buys availability and durability and gives you nothing to read; the asynchronous arrow (cyan) buys read capacity and gives you something that lags. Everything reports into CloudWatch, where ReplicaLag, DatabaseConnections and the RDS failover events are the signals you alarm on. The badges map the six things that actually page you — stale-serving after a failover, the triggers, the sync/HA boundary, backups, replica lag, and cross-Region cost — onto the exact node where each bites, and the legend narrates each as symptom · confirm · fix.

RDS reliability topology comparing Multi-AZ and read replicas: an application connects through a single stable DB endpoint CNAME to a primary in AZ-a, which replicates synchronously to a non-readable standby in AZ-b for high availability (the DNS target on failover) with backups taken from that standby, and asynchronously to readable read replicas and a cross-Region replica for read scaling and disaster recovery, all reporting ReplicaLag, DatabaseConnections and failover events into CloudWatch, with numbered badges marking the endpoint CNAME flip, failover triggers, the synchronous HA boundary, standby backups, asynchronous replica lag, and cross-Region cost

Real-world scenario

Finlytics, a Bengaluru fintech, ran its transaction ledger on a single RDS for PostgreSQL db.r6g.xlarge in ap-south-1. The team of eight had shipped fast and skipped the reliability homework: single-AZ, one instance, no replicas, backups at 7-day retention. It worked — until a Tuesday afternoon when the instance’s underlying host failed. RDS auto-recovered the instance on new hardware, but that took roughly nine minutes, during which every payment failed. The post-mortem action item was blunt: “make the database survive an AZ.” Someone enabled Multi-AZ, and the box on the reliability checklist got ticked.

Two weeks later the second incident arrived from the opposite direction. The finance team’s month-end reporting job — a set of heavy analytical queries — ran against the production primary and pinned its CPU at 100% for twenty minutes, and live payments slowed to a crawl because the reporting queries were starving the OLTP workload. An engineer’s fix was to “read from the Multi-AZ standby we set up.” It didn’t work: the standby refused connections. Confusion followed — “we have a replica, why can’t we read it?” — until the architect explained the distinction on a whiteboard. The Multi-AZ standby is not readable. They had bought availability and mistaken it for scalability.

The correct design separated the two concerns explicitly. Multi-AZ stayed on for HA (RPO ≈ 0, ~90 s failover, and — a bonus nobody had noticed — backups now came off the standby, ending the nightly latency blip). For read scaling, they added two read replicas: db.r6g.large each, one dedicated to the reporting/BI workload and one for read-heavy API traffic. The reporting job repointed to its replica; the primary’s CPU during month-end dropped from 100% to about 35%. They watched ReplicaLag and alarmed at 30 seconds — and immediately learned lesson three: during the month-end write burst plus the heavy report queries, the reporting replica’s lag climbed to 40 seconds, so a freshly-posted transaction sometimes didn’t appear in a report run seconds later. The fix was to size that replica up to db.r6g.xlarge and schedule the heaviest reports slightly off the write peak; lag settled under 5 seconds.

Then came the failover test that mattered. Before trusting Multi-AZ, they ran reboot-db-instance --force-failover in a staging clone during business hours — and their Java payment service went dark for eleven minutes even though RDS reported the failover complete in 80 seconds. Root cause: the JVM had cached the old primary’s IP (a networkaddress.cache.ttl of -1 under a SecurityManager), so it kept dialing the dead host. Setting the DNS TTL to 5 seconds, adding connection-pool validation, and wrapping transient errors in retry-with-backoff turned the next force-failover into a 12-second blip users never reported. Finally, for regulatory DR, they added a cross-Region read replica in ap-south-2 they could promote — accepting the cross-Region transfer cost and a documented RPO equal to its lag.

The migration as a timeline, because the order of realisations is the lesson:

Phase Symptom Action taken Result What it should have been
Baseline Single-AZ; host failure = 9 min outage (original design) Fragile Multi-AZ from day one
Fix 1 “Make it survive an AZ” Enable Multi-AZ HA achieved (RPO ≈ 0) Correct — but only half the story
Incident 2 Reporting starves the primary “Read from the standby” Fails — standby not readable Add read replicas, not read the standby
Fix 2 Need read offload Two read replicas (report + API) Primary CPU 100% → 35% The right tool for read scaling
Incident 3 Reports show stale data Found replica lag 40 s Size replica up; reschedule reports Watch ReplicaLag; size replicas
Incident 4 Failover test = 11 min app outage JVM cached dead IP Set DNS TTL 5 s, pool validation, retry Client must be failover-ready
DR Regional compliance Cross-Region replica (promotable) Documented RPO = lag Warm DR, promotion-based

The lesson on the wall: “Multi-AZ is for staying up; read replicas are for scaling out; and neither works until the client is built to reconnect.”

Advantages and disadvantages

Each mechanism earns its place by answering one question well — and disappoints when asked the other. Weigh them honestly:

Mechanism Advantages Disadvantages
Multi-AZ instance Automatic failover; RPO ≈ 0 (synchronous); no app-visible endpoint change; backups off the standby; one flag to enable; protects against AZ/host/storage failure Standby not readable (idle spend); ~60–120 s failover; ~2× cost; same-Region only; small sync write-latency tax
Multi-AZ DB cluster Faster failover (~35 s); two readable standbys (read offload + reader endpoint); higher write throughput; 3-AZ durability ~3× cost; limited engines (MySQL 8.0.28+/PostgreSQL 13.4+); NVMe classes only; Provisioned only; not a simple toggle from an instance
Read replicas Scale reads horizontally (up to 15); reporting isolation; cross-Region + near-Region reads; promotable for cutover/DR; each can be Multi-AZ Asynchronous → lag & stale reads; no auto-failover; no managed reader endpoint (classic); promotion is one-way; per-replica + egress cost; do nothing for writes

When each matters: put Multi-AZ on every production database — availability is table stakes, and the standby-backups bonus is free durability. Reach for the Multi-AZ DB cluster when you also want faster failover and modest read offload in one topology and your engine is supported. Add read replicas when reads genuinely outgrow one instance, when reporting must not touch the OLTP primary, when users in another Region need local reads, or when you need a promotable migration/DR target. The recurring mistake is using availability and scalability interchangeably — that’s how a team “scales” onto a standby that won’t answer and “fails over” to a replica that never promotes.

Hands-on lab

Convert a single-AZ instance to Multi-AZ, add a read replica, watch ReplicaLag, force a failover and confirm the endpoint name stays stable while connections reset, then promote the replica. Uses a small class; ⚠️ RDS bills per hour and Multi-AZ doubles compute — run the teardown at the end. Run in CloudShell (Bash) with a DB subnet group that spans at least two AZs.

Step 1 — Variables and a single-AZ starting instance.

export AWS_PAGER=""
ID=ha-lab
SG=$(aws ec2 describe-security-groups --filters Name=group-name,Values=default \
  --query "SecurityGroups[0].GroupId" --output text)

aws rds create-db-instance \
  --db-instance-identifier ${ID}-src \
  --engine postgres --db-instance-class db.t4g.small \
  --allocated-storage 20 --storage-type gp3 \
  --master-username labadmin --manage-master-user-password \
  --backup-retention-period 1 \
  --no-multi-az --no-publicly-accessible --vpc-security-group-ids $SG
aws rds wait db-instance-available --db-instance-identifier ${ID}-src

Expected: after a few minutes the instance is available, MultiAZ: false. Note --backup-retention-period 1 — a value > 0 is required before you can create a read replica.

Step 2 — Convert it to Multi-AZ and watch the transition.

aws rds modify-db-instance --db-instance-identifier ${ID}-src \
  --multi-az --apply-immediately
# Poll until MultiAZ becomes true (the standby is syncing in the background)
aws rds describe-db-instances --db-instance-identifier ${ID}-src \
  --query "DBInstances[0].{MultiAZ:MultiAZ,Status:DBInstanceStatus,PrimaryAZ:AvailabilityZone,StandbyAZ:SecondaryAvailabilityZone}"

Expected: status goes modifyingavailable, and MultiAZ becomes true with a SecondaryAvailabilityZone populated — that’s your synchronous standby (which you can’t connect to).

Step 3 — Create a read replica.

aws rds create-db-instance-read-replica \
  --db-instance-identifier ${ID}-rep \
  --source-db-instance-identifier ${ID}-src \
  --db-instance-class db.t4g.small
aws rds wait db-instance-available --db-instance-identifier ${ID}-rep

Expected: a second instance appears whose ReadReplicaSourceDBInstanceIdentifier is ${ID}-src. It has its own endpoint — there is no managed reader endpoint for classic replicas.

Step 4 — Note the endpoints (the names you’ll prove stay stable).

aws rds describe-db-instances \
  --query "DBInstances[?starts_with(DBInstanceIdentifier,'${ID}')].{Id:DBInstanceIdentifier,Endpoint:Endpoint.Address,MultiAZ:MultiAZ,Role:ReadReplicaSourceDBInstanceIdentifier}" \
  --output table

Record the ${ID}-src endpoint address. Keep this — you’ll compare it after failover.

Step 5 — Watch ReplicaLag.

aws cloudwatch get-metric-statistics --namespace AWS/RDS \
  --metric-name ReplicaLag --dimensions Name=DBInstanceIdentifier,Value=${ID}-rep \
  --start-time $(date -u -d '30 min ago' +%FT%TZ 2>/dev/null || date -u -v-30M +%FT%TZ) \
  --end-time $(date -u +%FT%TZ) --period 60 --statistics Maximum

Expected: values at or near 0 on an idle lab. Under real write load this is the number that climbs — the replica’s health signal.

Step 6 — Force a failover and confirm the endpoint is stable.

# This deliberately fails the primary over to the standby (your failover test)
aws rds reboot-db-instance --db-instance-identifier ${ID}-src --force-failover
# Watch for the failover events
aws rds describe-events --source-identifier ${ID}-src --source-type db-instance \
  --duration 20 --query "Events[].Message"
# Re-check the endpoint address — the NAME is identical; only the IP behind it changed
aws rds describe-db-instances --db-instance-identifier ${ID}-src \
  --query "DBInstances[0].{Endpoint:Endpoint.Address,AZ:AvailabilityZone,Status:DBInstanceStatus}"

Expected: events include a Multi-AZ failover message; the endpoint address string is unchanged, but AvailabilityZone has flipped from AZ-a to AZ-b — proof that failover repoints DNS rather than renaming the endpoint. Any open connection was dropped; a client with reconnect logic recovers in ~60–120 s.

Step 7 — Promote the read replica (one-way).

aws rds promote-read-replica --db-instance-identifier ${ID}-rep --backup-retention-period 1
aws rds wait db-instance-available --db-instance-identifier ${ID}-rep
# It's now an independent primary — no longer replicating from the source
aws rds describe-db-instances --db-instance-identifier ${ID}-rep \
  --query "DBInstances[0].{Id:DBInstanceIdentifier,Role:ReadReplicaSourceDBInstanceIdentifier,Status:DBInstanceStatus}"

Expected: Role (the source identifier) is now null — the replica is a standalone, writable database. There is no command to re-attach it; promotion is permanent.

Validation checklist. You converted single-AZ → Multi-AZ (a standby you can’t read), added a readable replica (its own endpoint, lag you watch), forced a failover (endpoint name stable, AZ flipped), and promoted the replica (one-way). That sequence is the whole article. What each step proved:

Step What you did What it proves
2 Enable Multi-AZ HA via a hidden synchronous standby; not readable
3 Create a read replica Read scaling is a separate, readable instance
5 Watch ReplicaLag Async replication lags; it’s the health metric
6 reboot --force-failover Failover flips DNS; the endpoint name is stable
7 promote-read-replica Promotion is a one-way cutover, not a failover

Teardown (do this — Multi-AZ + a replica is three billed instances).

aws rds delete-db-instance --db-instance-identifier ${ID}-rep --skip-final-snapshot
aws rds delete-db-instance --db-instance-identifier ${ID}-src --skip-final-snapshot

Cost note. Two db.t4g.small instances (one of them Multi-AZ = effectively two nodes) for an hour are a few rupees; the risk is forgetting to delete — RDS bills per hour whether idle or not, and Multi-AZ doubles the compute line. Run the teardown.

Common mistakes & troubleshooting

This is the playbook — bookmark it. First the scannable table you read mid-incident, then the entries that cost the most time with full confirm-and-fix detail.

# Symptom Root cause Confirm (exact path / command) Fix
1 App down for minutes after failover, though RDS says it completed in ~90 s Client cached the dead primary’s IP (JVM DNS TTL) App logs show connects to old IP; networkaddress.cache.ttl is -1/high Set DNS TTL ≤ 5 s; pool validation; retry-with-backoff
2 App never recovers after failover No reconnect/retry; pool hands out dead connections Pool has no test-on-borrow; no retry logic Add connection validation + retry; bound connection lifetime
3 Reporting can’t connect to “the standby” Multi-AZ instance standby is not readable Trying to connect to a standby that has no endpoint Use a read replica (or Multi-AZ cluster reader endpoint) for reads
4 “We failed over to the replica” — but writes are down Read replicas don’t auto-fail-over Primary is dead; replica still read-only For HA use Multi-AZ; to activate a replica you must promote-read-replica
5 ReplicaLag climbing to tens of seconds Write burst / undersized replica / long txn / DDL CloudWatch ReplicaLag; source WriteIOPS; long queries Size replica up; smooth writes; isolate reporting; schedule DDL
6 Reads still overload the writer App uses the instance/writer endpoint for reads Connection string points at the primary endpoint Route reads to replica endpoints / reader endpoint (cluster/Aurora)
7 Adding replicas didn’t help write performance Replicas scale reads, not writes Writes still bottlenecked; WriteIOPS pinned Scale up the writer / Aurora / shard — not more replicas
8 Failover took ~2 minutes; users errored Multi-AZ instance failover is 60–120 s + no retry Failover event timestamps; no app retry Multi-AZ cluster (~35 s) or Aurora; add retry/backoff
9 Promoted a replica, now can’t undo it Promotion is one-way Replica’s source is now null Rebuild replication from the new primary if needed; plan promotions
10 Can’t create a read replica Source has backup_retention_period = 0 describe-db-instances shows retention 0 Set retention > 0 on the source first
11 Enabling Multi-AZ blocked / errored DB subnet group has only one AZ Subnet group spans a single AZ Add subnets in a second AZ to the DB subnet group
12 Cross-Region replica bill is unexpectedly high Cross-Region data transfer (egress) Cost Explorer shows inter-Region transfer Budget egress; keep cross-Region replicas for DR/locality only
13 Stale reads right after a write Read-your-own-write hit a lagging replica Read from replica while source just wrote Route read-after-write to the primary/writer
14 Storage-full on the primary during a write burst allocated_storage exhausted, autoscaling off FreeStorageSpace near 0; storage-full Enable storage autoscaling with a ceiling; grow now
15 PostgreSQL WAL/disk filling up A stuck/lagging replica retains WAL via its slot TransactionLogsDiskUsage / slot lag rising Fix or drop the lagging replica; watch slot metrics

The expanded form for the entries that bite hardest:

1 & 2. The app doesn’t recover after a failover even though RDS did. Root cause: AWS promoted the standby and repointed DNS in ~60–120 s, but your client kept talking to the dead primary — either because it cached the old IP (the JVM classic: networkaddress.cache.ttl = -1 under a SecurityManager caches DNS forever) or because the connection pool handed out already-severed connections and nothing retried. Confirm: App logs show repeated connection attempts to the old primary’s IP long after the failover event; check the JVM DNS TTL setting; check the pool for test-on-borrow and any retry wrapper. Fix: Cap DNS caching (java.security networkaddress.cache.ttl=5, or set it programmatically); enable connection validation (test-on-borrow / a validation query) so dead connections are discarded; bound connection max lifetime so the pool periodically re-resolves; and wrap database calls in retry-with-backoff on transient errors. This is what turns AWS’s 90-second failover into a blip users never see.

3 & 4. Confusing the standby with a readable/failover-able replica. Root cause: Two opposite errors from the same confusion — trying to read the non-readable Multi-AZ standby, or expecting a read replica to fail over automatically. Confirm: For (3), you’re pointing a reporting connection at “the standby” (which has no endpoint) and getting connection failures. For (4), the primary is down, the replica is fine but read-only, and no write path exists because nobody promoted anything. Fix: For reads, add a read replica (or use a Multi-AZ cluster / Aurora reader endpoint). For availability, use Multi-AZ (automatic). If you intend a replica to be a failover target, you must explicitly promote-read-replica — and even then it’s manual DR, not HA.

5. Runaway ReplicaLag. Root cause: Asynchronous replication can’t keep up — a write burst on the source, an undersized replica, a long-running transaction or heavy read query blocking apply, or a bulk DDL / index build replaying slowly. Confirm: CloudWatch ReplicaLag climbing; correlate with source WriteIOPS (burst), replica CPU/IO (undersized), or long queries (PG pg_stat_activity, MySQL SHOW PROCESSLIST); check deploy timing for DDL. Fix: Size the replica at least as large as the source; smooth or batch write bursts; isolate reporting to its own replica and tune the queries; schedule DDL off-peak and expect a lag window; for PostgreSQL, watch replication-slot metrics so a stuck replica doesn’t fill the source’s WAL disk.

6 & 7. Reads overload the writer / replicas didn’t help. Root cause: Either your app routes reads to the writer/instance endpoint (so replicas sit idle), or you added replicas hoping to fix a write bottleneck. Confirm: Inspect connection strings — are reads pointed at the primary endpoint? Check whether the bottleneck is WriteIOPS/write CPU (replicas won’t help) versus read load (replicas will). Fix: Explicitly route lag-tolerant reads to replica endpoints (or a reader endpoint on a cluster/Aurora, or via RDS Proxy / a Route 53 record). For a write bottleneck, scale the writer up, move to Aurora’s higher write ceiling, or shard — never more replicas.

8. Failover was too slow for the SLA. Root cause: A Multi-AZ instance takes ~60–120 s, and without client retry that whole window is a user-visible outage. Confirm: Compare the failover event timestamps to your SLA; check for app retry logic. Fix: Move to a Multi-AZ DB cluster (~35 s) or Aurora (often < 30 s) if the topology fits your engine; always add retry/backoff so the client rides over the window regardless.

9. Promotion surprise — no going back. Root cause: promote-read-replica permanently detaches the replica; there is no demote/re-attach. Confirm: The former replica’s source identifier is now null; it accepts writes. Fix: Treat promotion as a deliberate cutover. If you need the old topology, you rebuild replication from the new primary. Plan promotions (migrations, DR) with a runbook; never promote to “test.”

An error/status reference for the connection failures you’ll actually see during a failover or lag event:

Error (engine) When it appears Meaning Action
FATAL: the database system is starting up (PG) Right after failover New primary still in crash recovery Retry with backoff; it clears in seconds
FATAL: the database system is in recovery mode (PG) During failover Node not yet accepting connections Retry; ensure client reconnect logic
could not connect to server: Connection timed out Failover / dead IP Client hitting the old primary IP Fix DNS TTL; reconnect to re-resolved endpoint
server closed the connection unexpectedly Failover moment Existing connection severed Pool validation + retry
ERROR 2003 / 2013 (HY000) (MySQL) Failover Can’t connect / lost connection Reconnect; retry-with-backoff
read-only / cannot execute ... in a read-only transaction Read hit a replica Writing to a read-only replica Route writes to the primary/writer endpoint
remaining connection slots are reserved Post-failover reconnect storm Every client reconnecting at once Pool + jittered backoff; RDS Proxy
terminating connection due to conflict with recovery (PG) On a replica A replica query conflicted with apply Tune max_standby_*_delay; retry the read

Best practices

The alarms worth wiring before the next incident:

Alert on Metric / signal Threshold (starting point) Why it’s leading
Replica falling behind ReplicaLag > 30 s sustained Stale reads before users notice
Connection pressure DatabaseConnections > 80% of max_connections Predicts post-failover reconnect storms
Storage exhaustion FreeStorageSpace < 10% free Prevents read-only/outage
I/O saturation DiskQueueDepth > 5 sustained Write bursts that drive lag
A failover happened RDS event (failover category) Any occurrence You must know it happened
PG WAL/slot buildup TransactionLogsDiskUsage / slot lag Rising trend A stuck replica can fill the source disk
CPU saturation CPUUtilization > 80% sustained Undersized replica or overloaded primary

Security notes

The security controls mapped to what each defends and how to set it:

Control Mechanism Defends against How to enable
Encryption at rest KMS on instance + replicas Disk/snapshot exposure --storage-encrypted at create; per-Region key for X-Region replica
Encryption in transit Force SSL/TLS Network sniffing / MITM rds.force_ssl (PG) / require_secure_transport (MySQL)
Network isolation Private subnets + SG Direct internet access SG inbound from app SG on DB port; no public access
Least-privilege auth IAM DB auth / scoped users Over-broad or static credentials rds-db:connect; Secrets Manager rotation
Deletion protection deletion_protection = true Accidental drop of primary/DR replica Flag on the instance; keep PITR on
Control-plane audit CloudTrail on RDS APIs Unexpected promotion / Multi-AZ disabled Enable trails; alarm on PromoteReadReplica, ModifyDBInstance

Cost & sizing

What drives the bill for HA and read scaling, and how to right-size:

A rough monthly picture (ap-south-1, illustrative — always price for your Region/usage):

Configuration What you pay for Rough INR / month Fits Watch-out
Single-AZ db.t4g.micro (free tier) One burstable instance + 20 GB ~₹0 (12 mo) then ~₹1,200 Dev / tiny apps No HA at all
Single-AZ db.r6g.large One memory-opt instance + gp3 ~₹18,000–22,000 Small prod without HA One AZ failure = outage
Multi-AZ db.r6g.large 2 nodes (standby idle) + gp3 ~₹36,000–45,000 Standard production HA ~2×; standby not readable
Multi-AZ DB cluster db.r6gd.large 3 NVMe nodes (2 readable) ~₹55,000–70,000 HA + faster failover + read offload ~3×; limited engines
Multi-AZ + 1 read replica 2 HA nodes + 1 replica instance ~₹54,000–67,000 HA plus read scaling Replica lag to manage
Multi-AZ + 2 read replicas 2 HA nodes + 2 replicas ~₹72,000–90,000 Read-heavy production Cost scales per replica
Cross-Region read replica (add-on) 1 replica instance + inter-Region transfer ~₹18,000 + egress DR / near-Region reads Transfer cost + higher lag

Reserved Instances cut steady-state RDS cost substantially (1- or 3-year commit) once your topology is stable — apply them to the primary, the Multi-AZ standby, and long-lived replicas. The sizing lesson from Finlytics: Multi-AZ doubled the compute line but ended nine-minute outages, and the read replicas that fixed reporting cost less than the lost revenue of a starved payment path — the cheapest HA is the incident you don’t have.

Interview & exam questions

1. What is the difference between Multi-AZ and a read replica? Multi-AZ is a synchronous standby in another AZ for high availability — automatic failover, RPO ≈ 0, and the standby (in the classic instance deployment) is not readable. A read replica is an asynchronous, readable copy for read scaling — it lags, has its own endpoint, and never fails over automatically. Different jobs; you usually want both.

2. During a Multi-AZ failover, does the endpoint change? No. The DB endpoint’s DNS name stays the same; RDS repoints the record to the promoted standby, so only the IP behind it changes. This is why failover is fast (no data to move) and why client-side DNS caching and reconnect logic determine whether the app actually recovers.

3. Why won’t adding read replicas fix a write bottleneck? Read replicas replicate asynchronously and serve reads only — they add no write capacity. A write bottleneck needs a bigger writer (scale up), Aurora’s higher write ceiling, a Multi-AZ cluster’s optimised write path, or sharding.

4. What’s the difference between a Multi-AZ instance and a Multi-AZ DB cluster? The instance deployment has one non-readable synchronous standby and fails over in ~60–120 s. The cluster deployment has one writer + two readable standbys across three AZs, replicates semi-synchronously (ack from one of two readers), fails over in ~35 s, and offers a reader endpoint and higher write throughput — but only on supported engines (MySQL 8.0.28+, PostgreSQL 13.4+) and NVMe instance classes.

5. Your database failed over in 90 seconds but the app was down for 10 minutes. Why? The client didn’t recover — almost always a cached DNS entry (JVM networkaddress.cache.ttl too high) pointing at the dead primary, or a connection pool with no validation/retry. Fix DNS TTL (≤ 5 s), add pool validation, bound connection lifetime, and retry transient errors with backoff.

6. What is ReplicaLag and why does it matter? It’s the number of seconds a read replica is behind its source, a consequence of asynchronous replication. High lag means stale reads and, for a DR replica, a larger RPO if you promote it. Watch it in CloudWatch, alarm around 30 s, and route freshness-critical reads to the primary.

7. What happens when you promote a read replica? It becomes an independent, writable primary; replication from the source stops permanently and cannot be re-attached. It’s used for migration cutovers, sharding, and DR activation. The promoted database’s RPO equals whatever lag it had at the promotion instant.

8. Can a Multi-AZ standby serve read traffic? In the classic Multi-AZ instance deployment, no — the standby is passive and accepts no connections. Only a Multi-AZ DB cluster (two readable standbys) or Aurora exposes readable standbys via a reader endpoint. For read scaling on an instance deployment, add read replicas.

9. How do read replicas fit a disaster-recovery plan? A cross-Region read replica is a warm DR target: it receives changes asynchronously, and on a Region failure you promote it to become the primary in the surviving Region. RTO is the promotion + repoint time; RPO is the replication lag at failure. It’s DR, not HA — there’s no automatic failover.

10. What does Multi-AZ do for backups? In Multi-AZ, automated backups and snapshots are taken from the standby, so the primary avoids the I/O suspension/latency a single-AZ backup can cause. It’s a performance/durability bonus alongside the availability you enable it for.

11. When would you choose Aurora over RDS Multi-AZ + replicas? When you need faster failover (often < 30 s), near-zero reader lag (shared storage, < 100 ms), replicas that double as failover targets, a managed reader endpoint, or replica auto-scaling — Aurora’s decoupled compute/storage delivers all of these, at the cost of engine-compatibility limits and its pricing model.

12. What’s required on the source before you can create a read replica? Automated backups must be enabled — backup_retention_period > 0. With retention at 0 the create-replica call fails. (The DB subnet group must also span AZs for Multi-AZ, and cross-Region replicas need a destination-Region KMS key if encrypted.)

These map to AWS Certified Solutions Architect – Associate (SAA-C03)design resilient, high-performing architectures, including HA and read scaling — and to SysOps Administrator – Associate (SOA-C02) for the operational side (failover behaviour, monitoring, events). A compact cert-mapping for revision:

Question theme Primary cert Objective area
Multi-AZ vs read replica selection SAA-C03 Design resilient architectures (HA)
Failover behaviour & client recovery SOA-C02 Reliability & business continuity
Multi-AZ instance vs cluster SAA-C03 / DBS High availability & performance
Replica lag, promotion, DR SOA-C02 / DBS Monitoring; disaster recovery
Read routing & endpoints SAA-C03 High-performing architectures
Cross-Region replicas & cost SAA-C03 Cost-optimised, multi-Region design
Encryption, IAM, network isolation SCS / SAA-C03 Secure architectures

Quick check

  1. Your reporting team wants to run heavy queries without touching the OLTP primary, and separately you need the database to survive an AZ failure. Which mechanism solves each, and can one do both?
  2. A Multi-AZ instance fails over. Does your application’s connection string need to change? What determines how fast the app actually recovers?
  3. True or false: a read replica will automatically become the primary if the source database fails.
  4. Your read replica’s ReplicaLag climbs to 45 seconds during a nightly batch load, and a report run right after shows stale data. What’s happening and what are two fixes?
  5. You promoted a read replica to run a migration cutover, then realised you needed the old replication topology back. Can you re-attach it? What does this tell you about promotion?

Answers

  1. Read replicas solve read scaling (a readable copy for reporting); Multi-AZ solves availability (a synchronous standby with automatic failover). The classic Multi-AZ instance standby cannot do both because it isn’t readable — you’d add read replicas alongside Multi-AZ. A Multi-AZ DB cluster (readable standbys) or Aurora can combine faster failover with read offload in one topology.
  2. No — the endpoint name is stable; RDS repoints the DNS record to the promoted standby. How fast the app recovers is determined by the client: DNS caching (keep TTL ≤ 5 s), connection-pool validation, and retry-with-backoff. AWS gets a writable primary back in ~60–120 s, but the client only recovers when it re-resolves and reconnects.
  3. False. Read replicas never fail over automatically. To activate one you must manually promote-read-replica, which is a one-way cutover, not an HA failover. For automatic failover use Multi-AZ.
  4. Asynchronous replication can’t keep up with the write burst, so the replica lags and serves stale reads (read-after-write against a lagging replica). Fixes: size the replica up (at least as large as the source), smooth/schedule the batch load off the read peak, isolate reporting to its own replica, and route freshness-critical reads to the primary. Alarm on ReplicaLag.
  5. No — promotion is permanent; there is no demote/re-attach. To restore a topology you rebuild replication from the new primary. The lesson: treat promotion as a deliberate, planned cutover (migration or DR), never as a test or a reversible step.

Glossary

Next steps

You can now place Multi-AZ and read replicas precisely and build a database that both stays up and scales its reads. Build outward:

AWSRDSMulti-AZRead ReplicasHigh AvailabilityFailoverDatabasesSAA-C03
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