Replication is one of those topics that every backend engineer eventually has to deal with — usually right when the primary database starts sweating under load, or right after an outage nobody wants to repeat. This guide walks through everything you need to know about PostgreSQL replication: what it is, why you need it, how it actually works under the hood, and how to configure it yourself, step by step.
Table of Contents
- What Is a Read Replica?
- Why Read Replicas Are Needed
- Primary vs Replica: Key Differences
- How Replication Works
- Replication Flow
- Synchronous, Asynchronous & Semi-Synchronous Replication
- Replication Lag & Monitoring
- Types of Replication in PostgreSQL
- PostgreSQL Replication Architecture
- Replication Components — Detailed Breakdown
- Checking PostgreSQL Activity and Storage
- Primary Server Configuration
- Replica Server Configuration
- Setting Up a Replica (Local & Remote)
- Verifying and Testing Replication
- Summary
What Is a Read Replica?
A read replica is a live, continuously-updated copy of your primary PostgreSQL database that accepts read-only queries. It is not a static backup — it stays in near real-time sync with the primary by receiving and replaying the stream of changes the primary produces.
Any attempt to run INSERT, UPDATE, or DELETE on a replica will fail with an error like:
ERROR: cannot execute INSERT in a read-only transaction
This is by design. The replica exists purely to serve reads, while all writes continue to go through the primary.
Why Read Replicas Are Needed
As an application grows, a single database instance becomes a bottleneck and a single point of failure. Read replicas solve several real problems at once:
-
Read/write separation – offload
SELECT-heavy traffic away from the primary so writes aren't competing for resources. - High availability & failover – a replica can be promoted to primary if the original server goes down.
-
Backup offloading – run
pg_dumpor backup jobs against a replica instead of hammering the primary. - Real-time reporting/analytics – dashboards and BI tools can query a replica without impacting production traffic.
- Geographic distribution – place replicas closer to users in different regions to reduce read latency.
Primary vs Replica: Key Differences
| Aspect | Primary Server | Replica Server |
|---|---|---|
Writes (INSERT/UPDATE/DELETE) |
Allowed | Blocked (read-only) |
Reads (SELECT) |
Allowed | Allowed |
| Generates WAL | Yes | No — it receives WAL |
| Role | Source of truth | Follower, replays changes |
pg_is_in_recovery() |
Returns false
|
Returns true
|
| Config flag | Normal instance |
hot_standby = on, has standby.signal
|
| Can be promoted | N/A | Can be promoted to primary during failover |
How Replication Works
Every change made to a PostgreSQL database — an insert, update, delete, or schema change — is first written to the Write-Ahead Log (WAL) before it's applied to the actual data files. This is a core durability mechanism in PostgreSQL, and replication piggybacks directly on it.
In physical replication:
- The primary writes every change as a WAL record.
- A WAL sender process on the primary streams these WAL records to the replica.
- A WAL receiver process on the replica receives the records and writes them to its own WAL.
- The startup process on the replica continuously replays the WAL, applying the same changes to its local data files.
- The replica's data converges to match the primary, typically within milliseconds to seconds.
Because the replica is replaying the exact same WAL as the primary, it ends up as a faithful copy — including indexes, table structures, and data.
Replication Flow
A simplified view of the data flow:
Client Write Request
|
v
Primary Server --> WAL Generated --> WAL Sender Process
| |
| (streams over network)
| v
| WAL Receiver Process (Replica)
| |
| v
| Replica WAL written locally
| |
| v
| Startup Process replays WAL
| |
| v
| Replica data files updated
v
Client Read Request ----------> Can be served by Primary OR Replica
Synchronous, Asynchronous & Semi-Synchronous Replication
PostgreSQL supports three modes that control how strictly the primary waits for a replica before confirming a write:
Asynchronous Replication (default)
The primary commits a transaction and returns success to the client immediately, without waiting for the replica to confirm it received or applied the WAL. This is fast, but there's a small risk: if the primary crashes right after commit, the very latest transactions might not have reached the replica yet.
Synchronous Replication
The primary waits for at least one designated synchronous standby to confirm it has received (and optionally flushed) the WAL before acknowledging the commit to the client. This guarantees zero data loss for that transaction but adds latency, since every commit is gated on network round-trip time to the replica.
Configured via:
synchronous_standby_names = 'replica1'
synchronous_commit = on
Semi-Synchronous Replication
A middle ground: the primary waits for confirmation that the WAL has been received by at least one replica (not necessarily applied/flushed to disk), balancing durability and performance. PostgreSQL's synchronous_commit parameter has several levels (remote_write, remote_apply, on, local, off) that let you fine-tune exactly how much guarantee you want versus how much latency you're willing to accept.
| Mode | Data Loss Risk | Write Latency | Use Case |
|---|---|---|---|
| Asynchronous | Small window possible | Lowest | Reporting replicas, general read scaling |
| Semi-synchronous | Very low | Moderate | Balanced HA setups |
| Synchronous | None (for that replica) | Highest | Financial/critical systems requiring zero data loss |
Replication Lag & Monitoring
Replication lag is the delay between a change happening on the primary and that same change being visible on the replica. Some lag is normal and expected in asynchronous replication — the goal is to keep it small and to catch it if it grows.
Check connected replicas (run on primary)
SELECT pid, client_addr, state, sync_state, write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
Count active replicas
SELECT count(*) FROM pg_stat_replication;
Check replication lag in time (run on replica)
SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;
Check replication slots
SELECT slot_name, slot_type, active FROM pg_replication_slots;
Check WAL retained per slot
SELECT slot_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retained
FROM pg_replication_slots;
Check WAL directory size
SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
Check WAL receiver status (run on replica)
SELECT * FROM pg_stat_wal_receiver;
Why monitoring matters: if a replica falls too far behind and its replication slot retains too much WAL, the primary's disk can fill up. Persistent high lag is usually a sign of network issues, an underpowered replica, or heavy write volume outpacing the replica's replay speed.
Types of Replication in PostgreSQL
PostgreSQL supports a few distinct approaches, each suited to different needs:
| Method | Type | Notes |
|---|---|---|
| Streaming replication | Physical | Real-time binary replication of the WAL. The replica is a byte-for-byte copy of the primary. Most common choice for standard read replicas — simple, reliable, keeps the whole database in sync automatically. |
| Logical replication | Logical | Table-level replication of data changes via logical decoding of WAL. Enables selective replication, cross-version replication, and CDC pipelines (Debezium, Kafka). |
| WAL file shipping | Physical | Periodic shipping of completed WAL segments instead of continuous streaming — an older, higher-lag, file-based approach still seen in some legacy setups. |
| Cascading replication | Physical | Replicas stream WAL to other replicas instead of all pulling from the primary directly — reduces load on the primary when many replicas are needed. |
PostgreSQL Replication Architecture
A typical production architecture looks like this:
+--------------------+
| Primary Server |
| (Read + Write) |
+---------+----------+
| WAL Streaming
+-------------------+-------------------+
v v v
+--------------+ +--------------+ +--------------+
| Read Replica | | Read Replica | | Read Replica |
| 1 | | 2 | | 3 |
| (Read only) | | (Read only) | | (Read only) |
+--------------+ +--------------+ +--------------+
Reads get distributed across replicas (often via a load balancer, connection pooler like PgBouncer, or application-level routing), while all writes are funneled to the single primary.
Replication Components — Detailed Description
| Component | Description |
|---|---|
| WAL (Write-Ahead Log) | Sequential log of every change made to the database, written before the change is applied to data files. Forms the basis of both crash recovery and replication. |
| WAL Sender | A process on the primary, spawned per connected replica, that reads WAL and streams it over the network. |
| WAL Receiver | A process on the replica that receives streamed WAL from the primary and writes it locally. |
| Startup Process (Replay) | On the replica, continuously reads the locally-written WAL and applies (replays) the changes to data files. |
| Replication Slot | A primary-side bookmark that ensures WAL isn't removed until the corresponding replica has consumed it — prevents replicas from falling irrecoverably behind, but can cause disk bloat if a replica disconnects for a long time. |
pg_stat_replication |
A system view on the primary showing real-time status of every connected replica (lag, state, sync mode). |
standby.signal |
A marker file in the replica's data directory indicating it should start in standby (replica) mode. |
hot_standby |
A setting that allows the replica to serve read-only queries while it's replaying WAL, instead of being completely inaccessible. |
pg_basebackup |
The utility used to take a full physical copy of the primary's data directory as the starting point for a new replica. |
| Replication User | A dedicated PostgreSQL role with the REPLICATION privilege used exclusively for streaming WAL — never for application queries. |
Checking PostgreSQL Activity and Storage
Before setting up replication, it's good practice to understand your primary's current load and size.
Connections and active queries
SELECT count(*) FROM pg_stat_activity;
SELECT usename, application_name, count(*)
FROM pg_stat_activity
GROUP BY usename, application_name;
SELECT pid, usename, state, query
FROM pg_stat_activity
WHERE state = 'active';
SHOW max_connections;
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;
SELECT usename, count(*) FROM pg_stat_activity GROUP BY usename;
Database size
SELECT datname AS database_name,
pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
ORDER BY pg_database_size(datname) DESC;
Top 20 largest tables
SELECT schemaname,
relname AS table_name,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_relation_size(relid)) AS table_size,
pg_size_pretty(pg_indexes_size(relid)) AS index_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;
Primary Server Configuration
1. Locate the configuration file
-
Ubuntu (PostgreSQL 14):
/etc/postgresql/14/main/postgresql.conf -
Mac (Homebrew, PostgreSQL 14):
/opt/homebrew/var/postgresql@14/postgresql.conf -
Ubuntu (PostgreSQL 15):
/etc/postgresql/15/main/postgresql.conf
2. Check current replication-related settings
SHOW wal_level;
SHOW max_wal_senders;
SHOW wal_keep_size;
SHOW max_replication_slots;
SELECT name, setting
FROM pg_settings
WHERE name LIKE '%wal%' OR name LIKE '%replication%';
3. Update postgresql.conf
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GB
-
wal_level = replicaenables enough WAL detail for streaming replication. -
max_wal_senderscaps how many replicas can connect simultaneously. -
max_replication_slotscaps how many replication slots can exist. -
wal_keep_sizekeeps a buffer of WAL on disk so a temporarily disconnected replica can catch up.
4. Configure pg_hba.conf to allow replica connections
Local testing:
host replication replicator 127.0.0.1/32 md5
or
host replication all 127.0.0.1/32 scram-sha-256
Remote replica (example IP 192.168.1.50):
host replication replicator 192.168.1.50/32 md5
5. Reload / restart PostgreSQL
sudo systemctl restart postgresql
SELECT pg_reload_conf();
6. Create the replication user
SELECT rolname, rolreplication, rolcanlogin FROM pg_roles;
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'your_secure_password';
| Permission | Meaning |
|---|---|
LOGIN |
Can connect to the database |
REPLICATION |
Can stream WAL files |
Replica Server Configuration
Enable hot standby mode so the replica can serve read queries while replaying WAL:
hot_standby = on
Also confirm a standby.signal file exists in the replica's data directory — this is what tells PostgreSQL to start the instance as a standby rather than a primary. pg_basebackup with the -R flag creates this automatically.
Setting Up a Replica (Local & Remote)
Local Replica (Same Server)
# Create data directory
mkdir ~/pg_replica
chmod 700 ~/pg_replica
# Take a base backup from the primary
pg_basebackup -h 127.0.0.1 -D ~/pg_replica -U replicator -P -R
# Change the replica's port to avoid conflicts
nano ~/pg_replica/postgresql.conf
port = 5433
hot_standby = on
# Start the replica
pg_ctl -D ~/pg_replica start
# Stop the replica
pg_ctl -D ~/pg_replica stop
Remote Replica (Different Server)
Make sure the primary allows remote replication (pg_hba.conf entry + firewall port 5432 open).
# On the replica server
mkdir ~/pg_replica
chmod 700 ~/pg_replica
# Base backup from the remote primary
pg_basebackup -h <PRIMARY_IP> -D ~/pg_replica -U replicator -P -R
# Configure postgresql.conf
port = 5432
hot_standby = on
# Start the replica
pg_ctl -D ~/pg_replica start
Tip: If
pg_ctl: command not found, locate your PostgreSQL binaries with:which psql sudo find / -name pg_ctl 2>/dev/nullTypical path:
/usr/lib/postgresql/14/bin/pg_ctl
Verifying and Testing Replication
On the primary — confirm the replica is connected
SELECT * FROM pg_stat_replication;
On the replica — confirm it's in recovery (standby) mode
SELECT pg_is_in_recovery(); -- should return true
Test that writes are blocked on the replica
INSERT INTO replication_test VALUES (2, 'test');
-- ERROR: cannot execute INSERT in a read-only transaction
Test that data actually replicates
On primary:
INSERT INTO test_table VALUES (1, 'test');
On replica:
SELECT * FROM test_table;
-- The row should appear here almost immediately
Summary
- A read replica is a live, read-only copy of your primary database kept in sync via WAL.
- Replication exists to scale reads, enable high availability, offload backups, and support real-time reporting.
- PostgreSQL supports physical (streaming), logical, and WAL file-shipping replication.
- Replication can run in asynchronous, synchronous, or semi-synchronous modes, trading off latency against durability guarantees.
- Core components — WAL, WAL sender/receiver, replication slots, and the startup/replay process — work together to keep replicas in sync.
-
pg_stat_replication,pg_stat_wal_receiver, andpg_replication_slotsare your primary tools for monitoring health and lag. - Setup is largely the same across PostgreSQL 14 and 15, on Ubuntu or macOS, for local or remote replicas: configure the primary (
wal_level,max_wal_senders,pg_hba.conf, replication role), configure the replica (hot_standby,standby.signal), take a base backup withpg_basebackup -R, and verify withpg_stat_replication/pg_is_in_recovery().
Replication is foundational to running PostgreSQL reliably at scale — once you understand the WAL-driven mechanics behind it, the rest (sync modes, monitoring, failover) all falls into place naturally.
Top comments (0)