Managed vs Self-Managed Read Replicas
You need read replicas, and the platform decision — a cloud-managed service like Amazon RDS, Aurora, or Google Cloud SQL versus streaming replicas you build yourself on EC2 or bare metal — sets the ceiling on every operational property that follows: how much of postgresql.conf you can touch, how failover behaves, how you see lag, what a replica costs, and how many replicas you can attach before the write path buckles. This decision sits underneath the database replication fundamentals and architecture that govern your whole deployment, and getting it wrong is expensive to reverse once application connection strings, monitoring, and failover runbooks have all been built against one model. This guide frames the trade-off precisely and gives you a configuration baseline and failure playbook for each side.
Concept Definition and Scope
A managed read replica is a replica whose lifecycle — provisioning, base backup, WAL streaming setup, patching, and (usually) failover — is owned by a cloud provider’s control plane. You interact with it through an API and a constrained parameter group, not a shell on the host. RDS for PostgreSQL and RDS for MySQL give you physical streaming replicas with a managed control plane; Aurora gives you shared-storage readers; Cloud SQL gives you managed streaming replicas with a similar constraint model.
A self-managed replica is a database you run yourself — a PostgreSQL standby streaming from a primary via primary_conninfo and a replication slot, or a MySQL replica configured with CHANGE REPLICATION SOURCE — on infrastructure you fully control (EC2, GCE, or physical servers). You own root, postgresql.conf, the WAL archive, the monitoring stack, and every line of the promotion runbook.
The dividing line is control surface versus operational burden. Everything a managed platform does for you is something it also decides for you. This guide scopes the comparison to read-scaling replicas specifically — not high-availability standbys, though the two overlap when a read replica is promoted during failover. For the deeper theory behind what you are replicating, see understanding synchronous vs asynchronous replication; nearly all read replicas on both sides of this decision are asynchronous.
Mechanism Deep-Dive
The two models diverge most sharply in who owns the WAL apply path and the promotion sequence. The diagram below contrasts the control boundaries: what lives inside the provider’s opaque control plane on the managed side, versus what you operate directly on the self-managed side.
On the managed side, the WAL apply pipeline runs behind the control plane. You cannot strace the startup process, you cannot pin apply parallelism beyond what the parameter group exposes, and you cannot inspect a stuck recovery with a shell. In exchange, you never write an archive_command, never manage slot bloat by hand, and get a documented failover contract with a bounded RTO.
On the self-managed side, a replica is just PostgreSQL in hot_standby mode. You control primary_conninfo, the physical or logical replication slot, max_standby_streaming_delay, hot_standby_feedback, and the promotion tooling (Patroni, repmgr, or a hand-rolled pg_ctl promote script). That control is what lets you tune around a pathological workload — and what obligates you to be the one holding the pager when a slot fills the primary’s disk.
A representative annotated baseline for a self-managed PostgreSQL standby:
# postgresql.conf on a self-managed streaming replica
hot_standby = on # allow read queries during recovery
primary_conninfo = 'host=primary.internal user=replicator application_name=replica01'
primary_slot_name = 'replica01_slot' # physical slot prevents WAL removal before replay
hot_standby_feedback = on # tell primary our oldest snapshot to avoid query cancels
max_standby_streaming_delay = 30s # cap query pause before WAL replay wins
wal_receiver_status_interval = 5s # how often we report replay position upstream
recovery_min_apply_delay = 0 # set >0 only for a deliberate delayed replicaEvery one of those lines is a knob a managed platform either hides entirely or exposes only through a parameter group with provider-imposed limits.
Trade-Off Comparison Table
This is the decision matrix. It weighs the axes that actually change on-call load and cost, not marketing feature checklists.
| Axis | Managed RDS (Postgres/MySQL) | Aurora | Self-managed streaming |
|---|---|---|---|
postgresql.conf access |
Parameter group, modifiable GUCs only | Cluster/instance parameter groups | Full file access, any GUC |
| Replication slot control | Limited; managed by control plane | N/A (shared storage) | Full — create, monitor, drop slots |
hot_standby_feedback |
Exposed as a parameter | Not applicable | Direct in postgresql.conf |
| Failover automation | Automated, ~60–120 s typical RTO | Automated, often under 30 s | You build it (Patroni/repmgr) |
| Lag visibility | CloudWatch ReplicaLag, 60 s granularity |
AuroraReplicaLag, ~sub-second |
Your exporter, arbitrary granularity |
| Typical steady-state lag | 10s–100s ms | Single-digit to low-tens ms | 10s–100s ms (tunable) |
| Max read replicas | 15 (RDS Postgres), 5 (RDS MySQL default) | 15 Aurora readers | Limited by primary WAL sender budget |
| Cross-region replicas | Supported, provider-managed | Global Database (managed) | You build streaming over WAN |
| Cascading replicas | RDS supports read-replica-of-replica | Reader tier only | Fully supported (chained standbys) |
| Patching / minor upgrades | Provider-scheduled maintenance window | Provider-managed | You own it |
| Cost model | Per-instance premium + storage + I/O | Per-instance + storage + I/O + IOPS | Instance + EBS + your engineering time |
| Failure blast radius you debug | Metrics + support ticket | Metrics + support ticket | Full shell, full logs, full ownership |
The pattern is consistent: managed platforms trade tuning depth and per-instance price for absorbed operational burden and a bounded failover contract. Self-managed trades engineering time and on-call surface for total control and lower per-instance cost at scale. Aurora is a third point — it gives up the streaming model entirely for shared storage, buying dramatically lower lag and fast failover at the cost of AWS lock-in and an I/O-based pricing dimension.
For a head-to-head on the two most common comparisons within this space, see RDS read replicas vs self-managed PostgreSQL replicas and Aurora replicas vs RDS read replicas for read scaling.
Configuration Runbook
Managed: create an RDS read replica and constrain it
# Create a physical read replica of an existing RDS Postgres instance
aws rds create-db-instance-read-replica \
--db-instance-identifier appdb-replica-1 \
--source-db-instance-identifier appdb-primary \
--db-instance-class db.r6g.xlarge \
--parameter-group-name appdb-replica-readonly \
--no-auto-minor-version-upgradeEnforce read-only intent and expose hot_standby_feedback through the parameter group — the only lever you have, since there is no postgresql.conf to edit:
aws rds modify-db-parameter-group \
--db-parameter-group-name appdb-replica-readonly \
--parameters \
"ParameterName=hot_standby_feedback,ParameterValue=1,ApplyMethod=immediate" \
"ParameterName=max_standby_streaming_delay,ParameterValue=30000,ApplyMethod=immediate"Self-managed: bring up a streaming standby with a slot
# On the primary — create the physical slot BEFORE base backup
psql -h primary.internal -c \
"SELECT pg_create_physical_replication_slot('replica01_slot');"
# On the replica host — base backup that writes recovery config + slot wiring
pg_basebackup -h primary.internal -U replicator -D /var/lib/postgresql/data \
-R --slot=replica01_slot -C --checkpoint=fast -PThe -R flag writes primary_conninfo and primary_slot_name into the data directory automatically, and -C --slot creates the slot atomically with the backup so no WAL is lost in the gap. This is the exact wiring a managed control plane performs for you invisibly.
Both: enforce read-only routing at the pool
Regardless of platform, application traffic should reach replicas through a read-only pool. See configuring PgBouncer for read-only connection pools for the pool-side enforcement, and ORM middleware for automatic query routing for the in-process split. The platform decision does not change the routing layer — it only changes what endpoint you point the pool at (a provider-supplied reader endpoint versus your own load-balanced replica VIP).
Monitoring and Alerting Signals
Lag visibility is where the two models feel most different day to day.
Managed surfaces lag as a single CloudWatch metric per replica, ReplicaLag, published at 60-second granularity (Aurora publishes AuroraReplicaLag at roughly one-second granularity). You alert on it in CloudWatch or scrape it into Prometheus via the CloudWatch exporter. You do not get pg_stat_replication byte deltas or apply-queue internals — the metric is wall-clock lag only.
# Prometheus alert on CloudWatch-exported RDS replica lag
groups:
- name: rds_replica_lag
rules:
- alert: RDSReplicaLagHigh
expr: aws_rds_replica_lag_average > 5
for: 3m
labels:
severity: warning
annotations:
summary: "RDS replica {{ $labels.dbinstance_identifier }} lag > 5 s"Self-managed gives you the full internal picture. Scrape pg_stat_replication from the primary and pg_last_wal_replay_lsn() from each replica, and pair it with a heartbeat table for wall-clock truth:
-- On the primary: byte-level queue depth per replica
SELECT application_name,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_bytes_behind,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;Whichever platform you choose, treat missing lag telemetry as itself alertable. For the full detection stack, see detecting and handling replication lag in real time and the SLA-threshold math in how to calculate replication lag thresholds for SLA compliance.
Failure Modes and Recovery Steps
1. Managed failover promotes a replica you were reading from
Symptom: During an RDS Multi-AZ failover or a manual promotion, a read replica is promoted to a standalone writable instance. Your read pool keeps sending traffic to it, and writes that should never have reached it start succeeding.
Recovery:
- Detect promotion via the
RDS-EVENT-0015/0016events or by pollingpg_is_in_recovery()returningfon a supposed replica. - Immediately re-point the read pool away from the promoted node’s endpoint.
- Rebuild a fresh read replica from the new primary; a promoted replica does not automatically rejoin as a reader.
2. Self-managed replication slot fills the primary disk
Symptom: A standby disconnects (crash, network partition, or long maintenance) but its physical slot remains. The primary retains all WAL since the slot’s restart_lsn, and pg_wal grows until the volume fills, threatening the entire cluster.
Recovery:
- Check
SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) FROM pg_replication_slots;. - If the replica is unrecoverable, drop the slot:
SELECT pg_drop_replication_slot('replica01_slot');. - Prevent recurrence by setting
max_slot_wal_keep_sizeso the primary invalidates a runaway slot before disk exhaustion rather than crashing.
3. Parameter you need is non-modifiable on managed
Symptom: A workload needs a GUC — say an aggressive max_standby_streaming_delay or a shared_preload_libraries entry — that the provider marks non-modifiable, and query cancellations or missing extensions block the migration.
Recovery:
- Confirm whether the parameter is truly non-modifiable via
describe-engine-default-parametersor the parameter group console. - If it is a hard limit, evaluate whether the workload belongs on self-managed instead, or whether an application-side mitigation (retry on
40001/cancel, or routing that read to the primary) is acceptable. - File the requirement as a platform constraint in your architecture decision record so the next team does not rediscover it during an incident.
4. Cross-region managed replica lag exceeds SLA during a traffic surge
Symptom: A cross-region RDS replica’s ReplicaLag climbs into the tens of seconds during a write spike in the source region, and regional read locality silently degrades to stale data.
Recovery:
- Route freshness-sensitive reads back to the source region temporarily while lag recovers.
- Verify the source is not WAL-sender-bound and that the destination instance class has headroom for apply.
- Review the topology against designing multi-region read replica topologies and the cost/lag math in the cross-region guide below.
Guides in This Section
The following guides go deep on the specific decisions summarized above.
RDS vs Self-Managed PostgreSQL
RDS Read Replicas vs Self-Managed PostgreSQL Replicas
A direct head-to-head on what RDS’s managed streaming gives and hides — limited GUC access, evolving replication-slot control, CloudWatch ReplicaLag — versus the full-control, full-ownership model of a hand-built PostgreSQL standby. Includes migration and cost considerations.
Aurora vs RDS for Read Scaling
Aurora Replicas vs RDS Read Replicas for Read Scaling Compares Aurora’s shared-storage readers (sub-10 ms lag, up to 15 readers, a reader endpoint, fast failover) against RDS physical streaming replicas (independent storage, cross-region reach, replica-of-replica) across read-scaling, lag, failover, and cost.
Cross-Region Cost and Lag
Cross-Region Read Replicas on RDS: Cost and Lag Tradeoffs
Breaks down the WAN lag floor, inter-region data-transfer cost, and the use cases — regional read locality and disaster recovery — that justify a cross-region replica, plus how to monitor ReplicaLag across regions and promote during a regional outage.
FAQ
Can I set hot_standby_feedback on a managed RDS replica?
On RDS for PostgreSQL, hot_standby_feedback is exposed as a modifiable parameter in the replica’s DB parameter group, so you can turn it on. What you cannot do is edit postgresql.conf directly or set GUCs the provider marks as non-modifiable. On Aurora the parameter is largely irrelevant because readers share the writer’s storage volume rather than replaying WAL.
How much operational work does self-managed replication actually add?
Expect to own base backups, replication slot lifecycle, WAL archiving, monitoring exporters, patching, and failover automation. That is typically one to two engineer-weeks of build plus ongoing on-call for promotion and slot-bloat incidents. Managed platforms absorb all of that in exchange for a per-instance premium and reduced tuning access.
Which option gives lower and more predictable replica lag?
Aurora replicas share a storage volume with the writer and typically show single-digit to low-tens-of-milliseconds lag. RDS physical streaming and well-tuned self-managed streaming both land in the tens-to-hundreds of milliseconds range in steady state. Self-managed wins only when you need apply-tuning knobs the managed platform hides.
Related
← Back to Database Replication Fundamentals & Architecture
- RDS Read Replicas vs Self-Managed PostgreSQL Replicas — the control-versus-convenience decision at the PostgreSQL level, with migration and cost detail.
- Aurora Replicas vs RDS Read Replicas for Read Scaling — shared-storage readers versus physical streaming replicas for horizontal read throughput.
- Cross-Region Read Replicas on RDS: Cost and Lag Tradeoffs — WAN lag floor, transfer cost, and DR promotion for geographically distributed reads.
- Designing Multi-Region Read Replica Topologies — how topology choices set your baseline lag floor across regions on either platform.
- Detecting and Handling Replication Lag in Real Time — the metrics and alerting stack you wire regardless of which platform you pick.