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.

Managed versus self-managed replica control boundaries On the managed side, the provider control plane owns provisioning, WAL streaming, failover, and patching; the engineer only sees an API and a parameter group and a CloudWatch lag metric. On the self-managed side, the engineer owns the replication slot, postgresql.conf, the WAL archive, the exporter, and the promotion script directly on the host. Managed (RDS / Aurora / Cloud SQL) Self-managed (EC2 / bare metal) API + console create-db-replica Parameter group modifiable GUCs only Provider control plane (opaque) provisioning · base backup WAL streaming · slot mgmt automated failover · patching no shell · no postgresql.conf CloudWatch ReplicaLag 60s granularity primary_conninfo + replication slot postgresql.conf full GUC access WAL archive archive_command postgres_exporter pg_stat_replication promote script Patroni / repmgr every box is yours to operate — and yours to page on Control you gain on the right is operational burden you also take on the abstraction on the left costs tuning depth and per-instance price

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:

ini
# 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 replica

Every 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

bash
# 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-upgrade

Enforce 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:

bash
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

bash
# 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 -P

The -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.

yaml
# 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:

sql
-- 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:

  1. Detect promotion via the RDS-EVENT-0015/0016 events or by polling pg_is_in_recovery() returning f on a supposed replica.
  2. Immediately re-point the read pool away from the promoted node’s endpoint.
  3. 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:

  1. Check SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) FROM pg_replication_slots;.
  2. If the replica is unrecoverable, drop the slot: SELECT pg_drop_replication_slot('replica01_slot');.
  3. Prevent recurrence by setting max_slot_wal_keep_size so 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:

  1. Confirm whether the parameter is truly non-modifiable via describe-engine-default-parameters or the parameter group console.
  2. 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.
  3. 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:

  1. Route freshness-sensitive reads back to the source region temporarily while lag recovers.
  2. Verify the source is not WAL-sender-bound and that the destination instance class has headroom for apply.
  3. 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.

← Back to Database Replication Fundamentals & Architecture