Skip to main content

Command Palette

Search for a command to run...

PostgreSQL Replication: Streaming Setup

Published
•10 min read•View as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Why Legacy Replication Approaches Fail Modern Requirements

The PostgreSQL replication landscape has evolved significantly. Older approaches using file-based log shipping with archive_command created replication lag measured in minutes because they relied on completing entire WAL (Write-Ahead Log) segments before shipping. This latency is unacceptable when applications need read replicas for real-time dashboards or when regulatory requirements mandate recovery point objectives (RPO) under 30 seconds.

Trigger-based replication solutions like Slony-I, once popular for selective table replication, introduce substantial overhead and complexity. They require maintaining trigger code across schema changes, create performance bottlenecks on write-heavy workloads, and struggle with DDL replication. In 2025, with microservices architectures generating thousands of schema migrations annually through tools like Flyway and Liquibase, trigger-based approaches create operational nightmares.

Even basic streaming replication setups from earlier PostgreSQL versions lack critical features now standard in production environments: synchronous replication with quorum-based commits, cascading replication for geographically distributed replicas, and logical replication for selective data distribution. Modern applications running on Kubernetes with automated scaling and multi-region deployments require replication configurations that adapt dynamically without manual intervention.

Modern PostgreSQL Streaming Replication Architecture

PostgreSQL streaming replication works by continuously streaming WAL records from a primary server to one or more standby servers. The standby servers apply these changes in real-time, maintaining a near-identical copy of the primary database. This architecture supports both physical replication (byte-level copying) and logical replication (row-level change streams), each serving distinct use cases.

Physical streaming replication creates exact replicas suitable for high availability and read scaling. The primary server streams WAL records through a persistent TCP connection, and standby servers apply changes continuously. This approach achieves replication lag typically under 100ms on properly configured networks, making replicas suitable for serving read queries that tolerate minimal staleness.

Logical replication, introduced in PostgreSQL 10 and significantly enhanced through version 17, enables selective replication of specific tables or databases. This proves essential for multi-tenant SaaS architectures where different customers' data must replicate to region-specific databases for compliance, or for feeding change data capture (CDC) pipelines that power real-time analytics systems.

Production-Grade Streaming Replication Configuration

Setting up streaming replication requires careful configuration of both primary and standby servers. Here's a production-ready configuration that addresses real-world requirements including security, monitoring, and failure recovery.

Primary Server Configuration

First, configure the primary server's postgresql.conf with appropriate replication parameters:

# Replication settings
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GB
hot_standby = on
synchronous_commit = remote_apply
synchronous_standby_names = 'FIRST 2 (standby1, standby2, standby3)'

# Performance tuning for replication
wal_sender_timeout = 60s
wal_receiver_timeout = 60s
wal_receiver_status_interval = 10s

# Monitoring and diagnostics
track_commit_timestamp = on

The synchronous_commit = remote_apply setting ensures transactions don't commit until changes are applied on standby servers, not just written to their WAL. This prevents scenarios where a failover promotes a standby that hasn't fully applied recent transactions, causing data visibility issues.

The synchronous_standby_names configuration uses quorum-based synchronous replication. The FIRST 2 syntax requires acknowledgment from any two of the three listed standbys before committing, balancing durability with availability. If one standby fails, commits continue with the remaining two.

Create a dedicated replication user with appropriate permissions:

CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'secure_password_from_vault';
GRANT USAGE ON SCHEMA pg_catalog TO replicator;

Configure pg_hba.conf to allow replication connections with certificate-based authentication:

# Replication connections
hostssl replication replicator 10.0.0.0/8 cert clientcert=verify-full

Certificate-based authentication prevents credential theft and integrates with modern secret management systems like HashiCorp Vault or AWS Secrets Manager.

Standby Server Initialization

Initialize the standby server using pg_basebackup, which creates a consistent snapshot while the primary remains online:

#!/bin/bash
set -euo pipefail

PRIMARY_HOST="primary.db.internal"
STANDBY_DATA_DIR="/var/lib/postgresql/17/main"
REPLICATION_SLOT="standby1_slot"

# Create replication slot on primary (idempotent)
psql -h $PRIMARY_HOST -U postgres -c \
  "SELECT pg_create_physical_replication_slot('$REPLICATION_SLOT') \
   WHERE NOT EXISTS (SELECT 1 FROM pg_replication_slots WHERE slot_name = '$REPLICATION_SLOT');"

# Stop PostgreSQL on standby if running
systemctl stop postgresql

# Remove existing data directory
rm -rf $STANDBY_DATA_DIR/*

# Create base backup with progress reporting
pg_basebackup -h $PRIMARY_HOST \
  -U replicator \
  -D $STANDBY_DATA_DIR \
  -Fp -Xs -P -R \
  -S $REPLICATION_SLOT \
  --checkpoint=fast \
  --wal-method=stream

# Set appropriate permissions
chown -R postgres:postgres $STANDBY_DATA_DIR
chmod 0700 $STANDBY_DATA_DIR

The -R flag automatically creates standby.signal and configures connection parameters in postgresql.auto.conf. The --wal-method=stream option streams WAL concurrently with the base backup, preventing WAL accumulation on the primary.

Standby Server Configuration

PostgreSQL 17 uses postgresql.auto.conf for replication parameters, but verify these settings:

# Standby configuration
primary_conninfo = 'host=primary.db.internal port=5432 user=replicator sslmode=verify-full sslcert=/etc/postgresql/certs/standby.crt sslkey=/etc/postgresql/certs/standby.key sslrootcert=/etc/postgresql/certs/ca.crt application_name=standby1'
primary_slot_name = 'standby1_slot'
hot_standby = on
hot_standby_feedback = on
max_standby_streaming_delay = 30s

The hot_standby_feedback = on setting prevents query cancellations on the standby by informing the primary about active queries. This trades slightly increased bloat on the primary for improved read replica stability—a worthwhile tradeoff for most applications.

Start the standby server:

systemctl start postgresql

Verify replication status on the primary:

SELECT application_name, state, sync_state, 
       pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS send_lag_bytes,
       pg_wal_lsn_diff(sent_lsn, write_lsn) AS write_lag_bytes,
       pg_wal_lsn_diff(write_lsn, flush_lsn) AS flush_lag_bytes,
       pg_wal_lsn_diff(flush_lsn, replay_lsn) AS replay_lag_bytes
FROM pg_stat_replication;

Implementing Automated Failover with Patroni

Manual failover procedures introduce human error and unacceptable downtime. Patroni, a mature high-availability solution for PostgreSQL, automates failover using distributed consensus through etcd, Consul, or ZooKeeper.

Here's a production Patroni configuration for a three-node cluster:

scope: production-cluster
namespace: /db/
name: node1

restapi:
  listen: 0.0.0.0:8008
  connect_address: node1.db.internal:8008
  authentication:
    username: patroni
    password: ${PATRONI_API_PASSWORD}

etcd3:
  hosts: etcd1:2379,etcd2:2379,etcd3:2379
  protocol: https
  cacert: /etc/patroni/certs/ca.crt
  cert: /etc/patroni/certs/client.crt
  key: /etc/patroni/certs/client.key

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      parameters:
        max_connections: 500
        shared_buffers: 8GB
        effective_cache_size: 24GB
        maintenance_work_mem: 2GB
        wal_level: replica
        max_wal_senders: 10
        max_replication_slots: 10
        synchronous_commit: remote_apply
        synchronous_standby_names: '*'

postgresql:
  listen: 0.0.0.0:5432
  connect_address: node1.db.internal:5432
  data_dir: /var/lib/postgresql/17/main
  bin_dir: /usr/lib/postgresql/17/bin
  authentication:
    replication:
      username: replicator
      password: ${REPLICATION_PASSWORD}
    superuser:
      username: postgres
      password: ${POSTGRES_PASSWORD}
  parameters:
    unix_socket_directories: '/var/run/postgresql'

tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false

Patroni continuously monitors cluster health and automatically promotes a standby when the primary fails. The maximum_lag_on_failover parameter prevents promoting standbys that are too far behind, avoiding data loss scenarios.

Deploy Patroni as a systemd service on each node:

[Unit]
Description=Patroni PostgreSQL Cluster Manager
After=network.target

[Service]
Type=simple
User=postgres
Group=postgres
Environment=PATRONI_API_PASSWORD=secure_password
Environment=REPLICATION_PASSWORD=secure_password
Environment=POSTGRES_PASSWORD=secure_password
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=always

[Install]
WantedBy=multi-user.target

Monitoring Replication Health

Effective monitoring prevents replication failures from causing outages. Track these critical metrics:

-- Replication lag in seconds
SELECT 
    application_name,
    client_addr,
    state,
    EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS lag_seconds,
    pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) / 1024 / 1024 AS lag_mb
FROM pg_stat_replication;

-- Replication slot status
SELECT 
    slot_name,
    slot_type,
    active,
    pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) / 1024 / 1024 AS retained_wal_mb,
    temporary
FROM pg_replication_slots;

-- Standby query conflicts
SELECT 
    datname,
    confl_tablespace,
    confl_lock,
    confl_snapshot,
    confl_bufferpin,
    confl_deadlock
FROM pg_stat_database_conflicts;

Integrate these queries with modern observability platforms like Grafana, Datadog, or New Relic. Set alerts for:

  • Replication lag exceeding 10 seconds
  • Inactive replication slots accumulating WAL beyond 10GB
  • Standby query conflicts exceeding 10 per minute
  • Synchronous standby count dropping below required quorum

Common Pitfalls and Edge Cases

Network partition handling: When network connectivity between primary and standby fails, synchronous replication can block all writes. Configure synchronous_commit = local as a fallback in postgresql.conf and use connection poolers like PgBouncer with health checks to detect and route around failed nodes.

WAL accumulation: Inactive replication slots prevent WAL cleanup, eventually filling disk space. Monitor slot activity and implement automated cleanup:

-- Drop inactive slots older than 1 hour
SELECT pg_drop_replication_slot(slot_name)
FROM pg_replication_slots
WHERE NOT active 
  AND slot_type = 'physical'
  AND pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) > 10737418240; -- 10GB

Timeline divergence: After failover, the old primary cannot rejoin without reconciliation. Use pg_rewind to resynchronize:

pg_rewind --target-pgdata=/var/lib/postgresql/17/main \
          --source-server="host=new-primary.db.internal user=postgres" \
          --progress

Cascading replication delays: When using cascading replication (standby replicating from another standby), failures in intermediate nodes break the chain. Configure multiple upstream sources in primary_conninfo using comma-separated hosts for automatic failover.

Certificate expiration: TLS certificates expire, breaking replication. Implement automated certificate rotation using cert-manager in Kubernetes or AWS Certificate Manager, and monitor certificate validity:

openssl x509 -in /etc/postgresql/certs/standby.crt -noout -enddate

Best Practices for Production Deployments

Use replication slots: Physical replication slots prevent WAL deletion before standbys consume it, eliminating a common cause of replication failures. Create slots before initializing standbys.

Implement connection pooling: Use PgBouncer or Pgpool-II to distribute read queries across replicas and handle failover transparently. Configure health checks that detect replication lag and remove lagging replicas from the pool.

Test failover regularly: Conduct monthly failover drills using chaos engineering tools like Chaos Mesh or Gremlin. Measure actual RTO and RPO against targets, and refine procedures based on results.

Separate read and write workloads: Use separate connection strings for read and write operations. This enables routing reads to replicas while ensuring writes always target the primary, preventing split-brain scenarios.

Monitor query conflicts: Long-running queries on standbys conflict with WAL replay, causing cancellations. Set max_standby_streaming_delay based on acceptable query cancellation rates, or route long-running analytics queries to dedicated replicas with higher delay tolerance.

Implement backup verification: Replication doesn't replace backups. Use pg_basebackup or pgBackRest for point-in-time recovery capability, and regularly test restoration procedures.

Plan for major version upgrades: Streaming replication requires identical PostgreSQL versions. Use logical replication for zero-downtime major version upgrades by replicating to a new cluster running the target version.

Secure replication traffic: Always use TLS with certificate verification for replication connections. Rotate certificates before expiration and use strong cipher suites (TLS 1.3 with ECDHE-RSA-AES256-GCM-SHA384 or better).

Frequently Asked Questions

What is the difference between synchronous and asynchronous PostgreSQL streaming replication?

Synchronous replication waits for standby acknowledgment before committing transactions, guaranteeing zero data loss during failover but adding latency. Asynchronous replication commits immediately without waiting, providing better performance but risking data loss if the primary fails before standbys receive recent changes. Use synchronous replication for financial transactions or compliance-critical data, and asynchronous for read scaling where eventual consistency is acceptable.

How does PostgreSQL streaming replication handle network failures in 2025?

Modern PostgreSQL configurations use replication slots to preserve WAL during network outages, preventing data loss when connectivity restores. Patroni and similar tools detect network partitions through distributed consensus and prevent split-brain scenarios by ensuring only one primary exists. Configure wal_keep_size and monitor slot lag to prevent disk exhaustion during extended outages.

What is the best way to scale read traffic with PostgreSQL replicas?

Deploy multiple asynchronous replicas behind a load balancer like HAProxy or use a connection pooler with read/write splitting. Configure application connection strings to route read queries to replicas while directing writes to the primary. Monitor replication lag and remove replicas exceeding acceptable staleness thresholds from the pool. For global applications, deploy replicas in multiple regions and route users to geographically nearest replicas.

When should you avoid using PostgreSQL streaming replication?

Avoid streaming replication for selective data distribution across different schemas or when replicating between different PostgreSQL versions. Use logical replication instead for these scenarios. Also avoid streaming replication for cross-cloud or high-latency networks where WAL streaming overhead becomes prohibitive—consider logical replication with batching or external CDC tools like Debezium.

How do you minimize replication lag in high-write workloads?

Optimize primary server performance with adequate shared_buffers, effective_cache_size, and fast storage (NVMe SSDs). Use multiple WAL senders by increasing max_wal_senders. On standbys, ensure sufficient CPU and I/O capacity for WAL replay. Consider parallel apply workers in PostgreSQL 16+ for logical replication. Monitor pg_stat_replication and tune wal_sender_timeout and wal_receiver_status_interval based on network characteristics.

What are the storage requirements for PostgreSQL streaming replication?

Standby servers require storage capacity matching the primary plus space for WAL retention. Configure wal_keep_size based on maximum expected replication lag and WAL generation rate. For a database generating 10GB WAL per hour with potential 4-hour outages, allocate at least 40GB for WAL retention. Use replication slots to prevent premature WAL deletion, but monitor slot lag to prevent unbounded growth.

How do you perform zero-downtime PostgreSQL major version upgrades with replication?

Set up logical replication from the old version primary to a new version standby. Once the new standby catches up, perform a controlled switchover by stopping writes, ensuring replication completes, then redirecting applications to the new primary. This approach requires application compatibility with both versions during the transition and careful handling of schema differences between versions.

Conclusion

PostgreSQL streaming replication provides the foundation for building resilient, scalable database architectures that meet modern availability and performance requirements. By implementing physical replication for high availability with sub