Skip to main content

PostgreSQL Streaming Replication

Guide for setting up PostgreSQL streaming replication with a primary-standby configuration, including optional Pgpool-II for connection pooling and automatic failover.

Overview​

Streaming replication provides:

  • High Availability: Automatic failover to standby server
  • Load Balancing: Distribute read queries across replicas
  • Data Redundancy: Real-time data replication
  • Zero Data Loss: With synchronous replication

Architecture​

┌─────────────┐ Replication ┌─────────────┐
│ Primary │ ─────────────────────────> │ Standby │
│ 192.168.1.51│ │ 192.168.1.52│
└─────────────┘ └─────────────┘
│ │
│ ┌─────────────┐ │
└──────────────│ Pgpool-II │─────────────┘
│ 192.168.1.50│
│ Port: 9999 │
└─────────────┘

Part 1: Primary Server Setup​

Step 1: Configure PostgreSQL on Primary​

Edit the main configuration file:

sudo nano /etc/postgresql/15/main/postgresql.conf

Add or modify these settings:

# Network
listen_addresses = '*'

# Replication settings
wal_level = replica
max_wal_senders = 10
wal_keep_size = 256

# Standby settings
hot_standby = on
wal_log_hints = on

# Synchronous replication (for zero data loss)
synchronous_commit = remote_apply
synchronous_standby_names = '*'

Configuration Explanation​

  • wal_level = replica: Enable WAL (Write-Ahead Log) for replication
  • max_wal_senders = 10: Maximum number of standby servers (adjust as needed)
  • wal_keep_size = 256: Keep 256MB of WAL files for standby servers
  • hot_standby = on: Allow read queries on standby servers
  • wal_log_hints = on: Required for pg_rewind
  • synchronous_commit = remote_apply: Wait for standby to apply changes
  • synchronous_standby_names = '*': All standbys are synchronous
Synchronous vs Asynchronous

Synchronous (remote_apply): Zero data loss, but slower commits Asynchronous: Faster, but potential data loss if primary fails

Step 2: Configure Authentication​

Edit the host-based authentication file:

sudo nano /etc/postgresql/15/main/pg_hba.conf

Add replication connection rules:

# TYPE DATABASE USER ADDRESS METHOD
host replication rep_user 192.168.1.52/32 scram-sha-256
host replication rep_user 192.168.1.0/24 scram-sha-256

Replace 192.168.1.52 with your standby server IP address.

Step 3: Create Replication User​

Connect to PostgreSQL and create the replication user:

psql
-- Create replication user
CREATE USER rep_user REPLICATION LOGIN ENCRYPTED PASSWORD 'YourSecurePassword';

-- Create monitoring user (optional, for Pgpool-II)
CREATE USER monitor WITH ENCRYPTED PASSWORD 'MonitorPassword';

-- Create monitoring database (optional)
CREATE DATABASE monitor;

-- Grant necessary privileges
GRANT pg_monitor TO monitor;

Step 4: Restart Primary Server​

Apply the configuration changes:

sudo systemctl restart postgresql

Verify it's running:

sudo systemctl status postgresql

Part 2: Standby Server Setup​

Step 1: Configure PostgreSQL on Standby​

Edit the configuration file:

sudo nano /etc/postgresql/15/main/postgresql.conf

Add these settings:

# Network
listen_addresses = '*'

# Primary connection info
primary_conninfo = 'host=192.168.1.51 port=5432 user=rep_user password=YourSecurePassword'

# Standby settings
hot_standby = on
Primary Connection String

The primary_conninfo tells the standby where to connect. Adjust the host, port, and credentials to match your primary server.

Step 2: Backup Existing Data​

Stop PostgreSQL and backup the existing data directory:

sudo systemctl stop postgresql
sudo mv /var/lib/postgresql/15/main /var/lib/postgresql/15/main-backup

Step 3: Initialize Standby from Primary​

Use pg_basebackup to copy data from the primary:

sudo -u postgres pg_basebackup \
-h 192.168.1.51 \
-p 5432 \
-U rep_user \
-Fp \
-Xs \
-P \
-R \
-D /var/lib/postgresql/15/main

Options Explanation​

  • -h: Primary server hostname/IP
  • -p: Primary server port
  • -U: Replication user
  • -Fp: Plain format (regular directory)
  • -Xs: Stream WAL files during backup
  • -P: Show progress
  • -R: Create standby.signal and write connection info
  • -D: Target data directory

You'll be prompted for the replication user's password.

Step 4: Set Permissions​

Fix permissions on the new data directory:

sudo chmod 0700 /var/lib/postgresql/15/main
sudo chown -R postgres:postgres /var/lib/postgresql/15/main

Step 5: Start Standby Server​

Start PostgreSQL on the standby:

sudo systemctl start postgresql
sudo systemctl status postgresql

Part 3: Verify Replication​

On Primary Server​

Check replication status:

psql -x -c "SELECT * FROM pg_stat_replication;"

Expected output:

-[ RECORD 1 ]----+------------------------------
pid | 12345
usesysid | 16384
usename | rep_user
application_name | walreceiver
client_addr | 192.168.1.52
client_hostname |
client_port | 54321
backend_start | 2025-10-20 10:00:00.000000+00
backend_xmin |
state | streaming
sent_lsn | 0/3000000
write_lsn | 0/3000000
flush_lsn | 0/3000000
replay_lsn | 0/3000000
write_lag |
flush_lag |
replay_lag |
sync_priority | 1
sync_state | sync

Key fields:

  • state: Should be streaming
  • sync_state: Should be sync (synchronous) or async (asynchronous)

On Standby Server​

Check if server is in recovery mode:

psql -c "SELECT pg_is_in_recovery();"

Should return t (true).

Test Replication​

On primary, create a test table:

CREATE TABLE replication_test (id INT, data TEXT);
INSERT INTO replication_test VALUES (1, 'Test data');

On standby, verify the data:

SELECT * FROM replication_test;

You should see the same data (standby is read-only).

Part 4: Pgpool-II Setup (Optional)​

Pgpool-II provides connection pooling, load balancing, and automatic failover.

Install Pgpool-II​

On a third server (or on primary/standby):

# Add PostgreSQL repository
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'

# Import repository key
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

# Move key to proper location
sudo mv /etc/apt/trusted.gpg /etc/apt/trusted.gpg.d/psql.gpg

# Update and install
sudo apt-get update
sudo apt-get install -y pgpool2 libpgpool2 postgresql-15-pgpool2

Configure Pgpool-II​

Edit the Pgpool configuration:

sudo nano /etc/pgpool2/pgpool.conf

Key configuration settings:

# Connection settings
listen_addresses = '*'
port = 9999
socket_dir = '/var/run/postgresql'

# Backend 0 (Primary)
backend_hostname0 = '192.168.1.51'
backend_port0 = 5432
backend_weight0 = 1
backend_flag0 = 'ALLOW_TO_FAILOVER'

# Backend 1 (Standby)
backend_hostname1 = '192.168.1.52'
backend_port1 = 5432
backend_weight1 = 1
backend_flag1 = 'ALLOW_TO_FAILOVER'

# Failover settings
failover_on_backend_error = on
auto_failback = on
auto_failback_interval = 1min

# Load balancing
load_balance_mode = on

Start Pgpool-II​

sudo systemctl enable pgpool2
sudo systemctl start pgpool2
sudo systemctl status pgpool2

Test Pgpool-II​

Check pool nodes:

psql -h 192.168.1.50 -U postgres -p 9999 postgres -c "SHOW pool_nodes;"

Expected output:

node_id | hostname | port | status | lb_weight | role | select_cnt
---------+---------------+------+--------+-----------+---------+------------
0 | 192.168.1.51 | 5432 | up | 0.500000 | primary | 0
1 | 192.168.1.52 | 5432 | up | 0.500000 | standby | 0

Managing Pgpool-II​

Manually Set Node Status​

If a node shows as down but is actually up:

# Edit status file
sudo nano /var/log/postgresql/pgpool_status

Change down to up for the affected node.

Debug Mode​

Run Pgpool in debug mode:

# Don't detach (stay in foreground)
pgpool -D

# With logging
pgpool -n &

View Logs​

sudo tail -f /var/log/postgresql/pgpool.log

Failover Testing​

Simulate Primary Failure​

On primary server:

sudo systemctl stop postgresql

Pgpool should automatically promote the standby to primary.

Check New Primary​

psql -h 192.168.1.50 -U postgres -p 9999 -c "SHOW pool_nodes;"

The former standby should now show as primary.

Manual Failover​

Promote standby to primary manually:

# On standby server
sudo -u postgres /usr/lib/postgresql/15/bin/pg_ctl promote -D /var/lib/postgresql/15/main

Monitoring​

Check Replication Lag​

On primary:

SELECT
client_addr,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;

Monitor WAL Files​

SELECT pg_current_wal_lsn(); -- On primary
SELECT pg_last_wal_receive_lsn(); -- On standby
SELECT pg_last_wal_replay_lsn(); -- On standby

Calculate Replication Delay​

SELECT
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS replication_delay_seconds;

Troubleshooting​

Replication Not Starting​

Check logs:

sudo tail -f /var/log/postgresql/postgresql-15-main.log

Common issues:

  • Incorrect pg_hba.conf entry
  • Wrong password in primary_conninfo
  • Network connectivity issues
  • Firewall blocking port 5432

Standby Falls Behind​

Increase wal_keep_size:

wal_keep_size = 1024 # Keep more WAL files

Or use replication slots:

-- On primary
SELECT * FROM pg_create_physical_replication_slot('standby_slot');
# On standby in primary_conninfo
primary_slot_name = 'standby_slot'

Connection Refused​

Check PostgreSQL is listening:

sudo ss -tlnp | grep postgres

Check firewall:

sudo firewall-cmd --add-service=postgresql --permanent
sudo firewall-cmd --reload

Best Practices​

  1. Use Synchronous Replication for critical data (zero data loss)
  2. Monitor Replication Lag regularly
  3. Test Failover procedures in development
  4. Regular Backups - replication is not a backup!
  5. Monitor Disk Space - WAL files can accumulate
  6. Use SSL for replication connections in production
  7. Document Failover Procedures for your team
  8. Set Up Alerts for replication failures

Security Enhancements​

Use SSL for Replication​

On primary, enable SSL:

ssl = on
ssl_cert_file = '/etc/postgresql/15/main/server.crt'
ssl_key_file = '/etc/postgresql/15/main/server.key'

Update primary_conninfo on standby:

primary_conninfo = 'host=192.168.1.51 port=5432 user=rep_user password=YourPassword sslmode=require'

Use Replication Slots​

Prevents primary from removing WAL files needed by standby:

-- On primary
SELECT * FROM pg_create_physical_replication_slot('standby1');

Update standby configuration:

primary_slot_name = 'standby1'

Additional Resources​