RDS Read Replicas vs Self-Managed PostgreSQL Replicas
Problem statement: You are choosing between Amazon RDS read replicas and PostgreSQL streaming replicas you run yourself on EC2 β and the two look interchangeable on a slide but diverge sharply the moment you need a specific postgresql.conf parameter, a replication slot you control, or lag telemetry finer than one data point per minute.
Symptom Identification
These are the concrete signals that push a team from one model to the other. If you are on RDS and hitting these, self-managed starts to look attractive:
- A required parameter (for example
shared_preload_librariesneeding an extension RDS does not package, or an aggressivemax_standby_streaming_delay) returns βThis parameter is not modifiableβ in the parameter group. - You need a physical replication slot you can inspect and drop for a downstream CDC pipeline, and the RDS engine version does not expose it.
- Your incident review needs
sent_lsn/replay_lsnbyte deltas, but all you have isReplicaLagat 60-second granularity. - You want a cascading replica (read replica of a read replica) with a custom apply delay that RDSβs cascade rules will not honor.
And the reverse β signals you are on self-managed and RDS would remove pain:
- Replication slot bloat has filled the primaryβs
pg_walvolume more than once and paged the on-call. - Patching, base backups, and minor-version upgrades consume real engineer-days each quarter.
- Your hand-rolled
pg_ctl promotefailover has a fencing gap that risks split-brain.
Root Cause Analysis
The two models are not the same product with different price tags β they draw the control boundary in a different place.
RDS read replicas are physical streaming replicas whose apply pipeline, slot lifecycle, WAL archive, and (with Multi-AZ) failover live inside AWSβs control plane. You configure them exclusively through DB parameter groups, and AWS marks a subset of GUCs non-modifiable to protect the managed contract. You get a documented create-db-instance-read-replica API, automated storage management, and ReplicaLag in CloudWatch β but no shell, no raw postgresql.conf, and historically limited replication-slot control that has expanded only gradually across engine versions.
Self-managed PostgreSQL replicas are ordinary standbys in hot_standby mode. Every knob is yours: primary_conninfo, primary_slot_name, hot_standby_feedback, recovery_min_apply_delay, apply parallelism, and the full promotion toolchain. In exchange you own base backups, WAL archiving, the monitoring exporter, patching, and the failover automation and its fencing logic.
The decision reduces to one question: does your workload need a control-plane knob that RDS hides, and is that need worth taking on the operational surface RDS absorbs? This is the specific PostgreSQL instance of the broader managed vs self-managed read replica trade-off, and it rests on the physical vs logical replication model you are running.
Architecture: Where the Control Boundary Sits
Step-by-Step Resolution
Use this procedure whether you are moving off RDS to self-managed or the reverse. The steps are ordered to surface blockers before you commit.
Step 1: Audit required parameters and extensions
List every GUC and extension your workload depends on, then check each against the RDS modifiable-parameter list for your engine version.
-- On the current primary: dump the non-default GUCs you actually rely on
SELECT name, setting, source
FROM pg_settings
WHERE source NOT IN ('default', 'override')
ORDER BY name;
-- And the extensions in use
SELECT extname, extversion FROM pg_extension ORDER BY extname;Inline verification: Every row in the first result must map to either a modifiable RDS parameter or an RDS-supported extension. Any that do not are hard blockers for the RDS direction.
Step 2: Map the replication-slot requirement
If you need explicit slot control β for CDC, a downstream logical subscriber, or slot-based lag monitoring β confirm what the target platform exposes.
-- Self-managed primary: create and inspect a physical slot
SELECT pg_create_physical_replication_slot('replica01_slot');
SELECT slot_name, slot_type, active, restart_lsn FROM pg_replication_slots;On RDS, slot capabilities depend on engine version and the rds.logical_replication parameter; verify before assuming parity.
Inline verification: On self-managed, pg_replication_slots shows your slot with active = t once the standby connects. On RDS, confirm slot creation succeeds without a permissions error.
Step 3: Reproduce lag visibility on the target
# RDS: read ReplicaLag directly
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS --metric-name ReplicaLag \
--dimensions Name=DBInstanceIdentifier,Value=appdb-replica-1 \
--start-time "$(date -u -d '10 min ago' +%FT%TZ)" \
--end-time "$(date -u +%FT%TZ)" \
--period 60 --statistics Average-- Self-managed: byte + wall-clock lag, arbitrary granularity
SELECT application_name,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS bytes_behind,
EXTRACT(EPOCH FROM replay_lag) AS replay_lag_s
FROM pg_stat_replication;Inline verification: RDS returns one datapoint per 60 s; the self-managed query returns per-scrape byte and second deltas. Confirm your alerting can consume whichever you land on.
Step 4: Establish the failover contract
On RDS, promotion is aws rds promote-read-replica or automatic during a Multi-AZ event. On self-managed, wire Patroni or repmgr with fencing so a partitioned old primary cannot accept writes.
# RDS manual promotion (breaks replication, makes it standalone-writable)
aws rds promote-read-replica --db-instance-identifier appdb-replica-1Inline verification: After promotion, SELECT pg_is_in_recovery(); must return f on the promoted node, and your read pool must no longer target it as a replica.
Step 5: Cut over reads behind a pool
Point your read-only pool at the new endpoint and drain the old one. Never re-point application code directly at replica hostnames β route through the pool so cutover is a config change, not a deploy. See configuring PgBouncer for read-only connection pools.
Inline verification: Through the pool, SELECT pg_is_in_recovery(); returns t and a test INSERT is rejected with cannot execute INSERT in a read-only transaction.
Configuration Snippet
Equivalent read-only enforcement on each platform:
# Self-managed: postgresql.conf on the standby
hot_standby = on
default_transaction_read_only = on
hot_standby_feedback = on
max_standby_streaming_delay = 30s# RDS: the same intent via parameter group (no file access)
aws rds modify-db-parameter-group \
--db-parameter-group-name appdb-replica-readonly \
--parameters \
"ParameterName=default_transaction_read_only,ParameterValue=1,ApplyMethod=immediate" \
"ParameterName=hot_standby_feedback,ParameterValue=1,ApplyMethod=immediate"Verification and Rollback
Confirm the chosen platform is serving reads correctly:
SELECT pg_is_in_recovery(); -- t on any replica
SHOW default_transaction_read_only; -- on
SELECT pg_last_wal_replay_lsn(); -- advancing over timeRollback (self-managed β RDS abandoned mid-migration): re-point the pool back to the RDS reader endpoint; RDS replicas continued streaming throughout, so no data was lost.
Rollback (RDS β self-managed abandoned): if the self-managed standby fell behind, drop its slot on the source (SELECT pg_drop_replication_slot('replica01_slot');) to stop WAL retention, then rebuild from a fresh base backup once the underlying blocker is resolved.
Edge Cases and Gotchas
RDS marks the parameter modifiable but ignores it on replicas
Some parameters are modifiable in the parameter group yet only take effect on the writer, or require a reboot the replica schedules on its own cadence. Always confirm the running value on the replica itself with SHOW <param>; after applying, rather than trusting the parameter-group state.
Self-managed slots silently retain WAL after a standby dies
A physical slot keeps restart_lsn pinned even when the standby is gone, and the primary retains all WAL since that point. Set max_slot_wal_keep_size so the primary invalidates the slot before pg_wal fills the disk β the single most common way self-managed replication takes down a primary.
Major-version upgrades diverge
RDS performs in-place major-version upgrades on its own schedule and can break replica compatibility mid-window; self-managed upgrades require you to rebuild or pg_upgrade the standby yourself. Neither is free β budget the upgrade path explicitly when you pick a platform, and cross-check it against your replication lag threshold SLAs.
FAQ
Can I create a logical replication slot on an RDS read replica?
Recent RDS for PostgreSQL versions expose logical replication and let you create logical slots on the primary with rds.logical_replication enabled, and newer versions allow logical slots on replicas too. Older engine versions did not give you slot control at all. Always confirm against your exact engine version, because this is one of the capabilities that has changed most across RDS releases.
How fine-grained is CloudWatch ReplicaLag compared to pg_stat_replication?
ReplicaLag is a single wall-clock-seconds value published at 60-second granularity. A self-managed setup scraping pg_stat_replication gives you sent, write, flush, and replay LSN deltas at whatever interval you scrape, typically 5 seconds, plus byte-level queue depth. RDS hides the byte deltas and the sub-minute detail.
Is self-managed always cheaper than RDS for replicas?
Not once you price in engineering time. Self-managed saves the RDS per-instance management premium, but you pay for base backups, monitoring, patching, and on-call. Self-managed wins on raw cost at large replica counts or when you already run a platform team; RDS wins for small teams where the premium is cheaper than the head count.
Related
β Back to Managed vs Self-Managed Read Replicas
- Aurora Replicas vs RDS Read Replicas for Read Scaling β if you are already on AWS, the shared-storage reader model may beat both RDS streaming and self-managed for pure read scaling.
- When to Use Logical vs Physical Replication for Read Scaling β the replication mode underneath both RDS and self-managed replicas, and what slot control buys you.
- Configuring PgBouncer for Read-Only Connection Pools β the pool layer that makes platform cutover a config change instead of a redeploy.
- Detecting and Handling Replication Lag in Real Time β building the lag telemetry RDS hides and self-managed makes you assemble.