DEV Community

Sai Praveen Sanapalli
Sai Praveen Sanapalli

Posted on

PostgreSQL Replication: The Complete Guide to Read Replicas, Architecture, and Setup

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

  1. What Is a Read Replica?
  2. Why Read Replicas Are Needed
  3. Primary vs Replica: Key Differences
  4. How Replication Works
  5. Replication Flow
  6. Synchronous, Asynchronous & Semi-Synchronous Replication
  7. Replication Lag & Monitoring
  8. Types of Replication in PostgreSQL
  9. PostgreSQL Replication Architecture
  10. Replication Components — Detailed Breakdown
  11. Checking PostgreSQL Activity and Storage
  12. Primary Server Configuration
  13. Replica Server Configuration
  14. Setting Up a Replica (Local & Remote)
  15. Verifying and Testing Replication
  16. 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
Enter fullscreen mode Exit fullscreen mode

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_dump or 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:

  1. The primary writes every change as a WAL record.
  2. A WAL sender process on the primary streams these WAL records to the replica.
  3. A WAL receiver process on the replica receives the records and writes them to its own WAL.
  4. The startup process on the replica continuously replays the WAL, applying the same changes to its local data files.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Count active replicas

SELECT count(*) FROM pg_stat_replication;
Enter fullscreen mode Exit fullscreen mode

Check replication lag in time (run on replica)

SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;
Enter fullscreen mode Exit fullscreen mode

Check replication slots

SELECT slot_name, slot_type, active FROM pg_replication_slots;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Check WAL directory size

SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
Enter fullscreen mode Exit fullscreen mode

Check WAL receiver status (run on replica)

SELECT * FROM pg_stat_wal_receiver;
Enter fullscreen mode Exit fullscreen mode

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)  |
 +--------------+   +--------------+   +--------------+
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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%';
Enter fullscreen mode Exit fullscreen mode

3. Update postgresql.conf

wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GB
Enter fullscreen mode Exit fullscreen mode
  • wal_level = replica enables enough WAL detail for streaming replication.
  • max_wal_senders caps how many replicas can connect simultaneously.
  • max_replication_slots caps how many replication slots can exist.
  • wal_keep_size keeps 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
Enter fullscreen mode Exit fullscreen mode

or

host    replication     all             127.0.0.1/32            scram-sha-256
Enter fullscreen mode Exit fullscreen mode

Remote replica (example IP 192.168.1.50):

host    replication     replicator      192.168.1.50/32         md5
Enter fullscreen mode Exit fullscreen mode

5. Reload / restart PostgreSQL

sudo systemctl restart postgresql
Enter fullscreen mode Exit fullscreen mode
SELECT pg_reload_conf();
Enter fullscreen mode Exit fullscreen mode

6. Create the replication user

SELECT rolname, rolreplication, rolcanlogin FROM pg_roles;

CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'your_secure_password';
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Tip: If pg_ctl: command not found, locate your PostgreSQL binaries with:

which psql
sudo find / -name pg_ctl 2>/dev/null

Typical 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;
Enter fullscreen mode Exit fullscreen mode

On the replica — confirm it's in recovery (standby) mode

SELECT pg_is_in_recovery();  -- should return true
Enter fullscreen mode Exit fullscreen mode

Test that writes are blocked on the replica

INSERT INTO replication_test VALUES (2, 'test');
-- ERROR: cannot execute INSERT in a read-only transaction
Enter fullscreen mode Exit fullscreen mode

Test that data actually replicates

On primary:

INSERT INTO test_table VALUES (1, 'test');
Enter fullscreen mode Exit fullscreen mode

On replica:

SELECT * FROM test_table;
-- The row should appear here almost immediately
Enter fullscreen mode Exit fullscreen mode

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, and pg_replication_slots are 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 with pg_basebackup -R, and verify with pg_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)