Replica lag is the delay between a write committing on the primary and that write being visible on a read replica, exposed as the ReplicaLag CloudWatch metric (seconds) for RDS MySQL, MariaDB, and Postgres, and as AuroraReplicaLag (milliseconds) for Aurora. Sub-second to a few seconds during write bursts is normal physics; sustained tens of seconds means the replica cannot keep up with the primary’s write rate; a lag that only ever grows — or a Seconds_Behind_Source of NULL on MySQL — means replication has stopped and the replica is now a slowly rotting snapshot. Each threshold has a different playbook.
What’s happening
On RDS MySQL and MariaDB, replicas apply the primary’s binary log. Historically the apply side was single-threaded — a primary committing on 16 cores funnels into a replica applying changes with far less parallelism, so a write-heavy burst on the primary physically cannot be replayed at the same rate. Modern engine versions support parallel apply (replica_parallel_workers and related parameters), which helps but does not repeal the asymmetry. Long transactions are the other classic: a 20-minute UPDATE on the primary appears in the binlog as work the replica must chew through, and lag spikes by roughly the transaction’s duration.
On RDS Postgres, replicas apply WAL. The distinctive failure mode is apply pauses: long-running queries on the replica conflict with incoming WAL (a vacuum cleanup on the primary wants to remove rows a replica query still reads), and the replica either kills your query or delays applying WAL, depending on max_standby_streaming_delay and whether hot_standby_feedback is on. Lag on a Postgres replica running heavy analytics is often self-inflicted by those very queries.
Aurora is different plumbing entirely — replicas share the storage layer, so AuroraReplicaLag measures page-cache propagation and normally sits in the low tens of milliseconds; sustained seconds on Aurora signals a genuinely overloaded reader.
Two universal aggravators: a replica on a smaller instance class or cheaper storage than its primary (it must do the same write work with less hardware, plus serve reads), and no binlog/WAL headroom, where lag turns into broken replication once the primary purges logs the replica has not yet fetched.
What to check first
ReplicaLagshape in RDS > Databases > your-replica > Monitoring — spikes that recover track write bursts; a staircase that never descends is a stopped or drowning replica.- Replica instance class and storage vs the primary — smaller replica is the quiet default mistake.
WriteIOPSon the primary overlaid on the lag — if lag rises with every write burst, it is throughput, not breakage.- Long transactions or batch jobs on the primary around lag onset; on Postgres, long queries on the replica too.
- Replication status from the engine (below) — lag numbers are meaningless if replication has actually stopped.
How to verify
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS --metric-name ReplicaLag \
--dimensions Name=DBInstanceIdentifier,Value=prod-db-replica1 \
--start-time 2026-07-04T00:00:00Z --end-time 2026-07-04T12:00:00Z \
--period 300 --statistics Maximum
On the replica itself:
-- MySQL 8.0+: state, lag, and any apply error
SHOW REPLICA STATUS\G
-- Key fields: Replica_IO_Running, Replica_SQL_Running,
-- Seconds_Behind_Source (NULL = broken), Last_SQL_Error
-- Postgres: is WAL being received and applied, and how far behind
SELECT status, receive_start_lsn FROM pg_stat_wal_receiver;
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;
On MySQL, Replica_SQL_Running: No with a populated Last_SQL_Error is the smoking gun for broken-not-slow — usually a write that reached the replica directly (someone wrote to a writable replica) or a data inconsistency.
The runbook
- Under ~5 seconds, intermittent: leave it alone, but route honestly. This is normal replication physics under bursty writes. The fix is application-side: any read-after-write path (user saves, then immediately views) must read from the primary. Session-pinning and stale-read tolerance patterns are covered in read replicas in a multi-tenant SaaS.
- Sustained 10-60 seconds: find the throughput mismatch. Match the replica’s instance class and storage configuration to the primary’s. Enable parallel apply on MySQL-family replicas (
replica_parallel_workerssized to cores,replica_preserve_commit_orderon). Break large batch writes on the primary into smaller transactions — one million-row UPDATE becomes a hundred 10k-row chunks. - Postgres-specific: resolve the conflict triangle. If lag rises while analytics run on the replica, choose deliberately:
hot_standby_feedback = onprotects replica queries but causes bloat on the primary; a largermax_standby_streaming_delaylets lag grow to protect queries; cancelling queries protects freshness. You get two of three — pick per replica role. - Minutes and climbing: triage as an incident. Stop non-essential writes or batch jobs on the primary, pause read traffic to the lagging replica (it is serving increasingly stale data), and watch whether lag plateaus. Check
FreeStorageSpaceon the primary — retained binlogs/WAL for a dead replica fill disks, which is its own emergency: RDS storage full. - NULL / stopped / errored: fix or rebuild. On MySQL, a replica stopped on an SQL error can sometimes be resumed (
CALL mysql.rds_start_replication;) after resolving the cause, but skipping errors buys silent divergence. Past the point where the primary purged the logs the replica needs, there is no repair — delete and recreate the replica. Rebuilding is boring and correct; nursing a divergent replica is neither. - Chronic lag with everything tuned: the workload outgrew the pattern. Options in ascending order of effort: another replica to split read load, moving analytics to a dedicated replica with relaxed freshness, or Aurora, whose shared-storage replicas eliminate apply lag as a category — the trade-offs are in Aurora vs RDS.
When this is the wrong move
Do not chase zero lag — asynchronous replication is asynchronous, and engineering for “always under one second” on RDS MySQL is a losing fight; if the application requires that guarantee, it requires reading from the primary or a different architecture (Multi-AZ readable standbys, Aurora). Do not “fix” lag by promoting the replica — promotion breaks replication permanently and is a topology change, not a repair. And on Aurora, do not apply binlog-era advice; the mechanism is different and the Amazon RDS documentation treats the two separately for good reason.
Common mistakes
- Running replicas one instance class smaller than the primary “because they only serve reads” — they replay every write and serve reads.
- Alerting on average lag: a five-minute average of 3 seconds hides 30-second spikes that break read-after-write flows.
- Treating
Seconds_Behind_Source: NULLas zero. NULL means broken. Zero means caught up. - Skipping replication errors on MySQL (
rds_skip_repl_error) repeatedly — each skip is a small act of data divergence you will meet again during a promotion.
Prevention
- Alarm on
ReplicaLagmaximum at a threshold your product actually tolerates (often 10-30 s), and separately on replication stopped. - Keep replica hardware and parameter groups matched to the primary by policy.
- Chunk batch writes and schedule heavy jobs off-peak; the replica replays everything you do to the primary.
- Set binlog retention (
binlog retention hours) generous enough to survive a replica being down over a weekend. - Test replica rebuild time — it is your real recovery number when replication breaks.
Replica lag is a throughput budget: the replica must replay every byte the primary writes, with hardware to spare for reads — get the budget right and the thresholds above mostly stay theoretical. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.