Set up PostgreSQL 17 streaming replication with PgBouncer connection pooling and load balancing

Advanced 45 min Apr 28, 2026 96 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure PostgreSQL 17 with streaming replication for high availability, then add PgBouncer connection pooling with intelligent load balancing across primary and replica servers for production-grade database infrastructure.

Prerequisites

  • At least 3 servers (1 primary, 2+ replicas)
  • Root or sudo access
  • Network connectivity between servers
  • Basic PostgreSQL knowledge

What this solves

PostgreSQL streaming replication creates real-time copies of your primary database on replica servers, providing automatic failover and read scaling. PgBouncer adds connection pooling and load balancing, distributing read queries to replicas while routing writes to the primary. This setup handles database high availability, reduces connection overhead, and scales read performance across multiple servers.

Step-by-step configuration

Install PostgreSQL 17 on all servers

Install PostgreSQL 17 on your primary server and all replica servers. You'll need at least two servers for this setup.

sudo apt update
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt update
sudo apt install -y postgresql-17 postgresql-client-17 postgresql-contrib-17
sudo dnf update -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf install -y postgresql17-server postgresql17-contrib postgresql17

Initialize PostgreSQL on primary server

Initialize the PostgreSQL database cluster on your primary server and start the service.

sudo systemctl enable postgresql
sudo systemctl start postgresql
sudo systemctl status postgresql
sudo /usr/pgsql-17/bin/postgresql-17-setup initdb
sudo systemctl enable postgresql-17
sudo systemctl start postgresql-17
sudo systemctl status postgresql-17

Configure primary server for replication

Edit the PostgreSQL configuration to enable streaming replication. This allows replica servers to connect and receive real-time database changes.

listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GB
archive_mode = on
archive_command = '/bin/true'
checkpoint_completion_target = 0.9
wal_compression = on

Configure authentication for replication

Set up PostgreSQL host-based authentication to allow replica servers to connect for replication.

# Add these lines at the end, replace with your replica server IPs
host replication replicator 203.0.113.11/32 md5
host replication replicator 203.0.113.12/32 md5
host all pgbouncer 203.0.113.10/32 md5
host all pgbouncer 203.0.113.11/32 md5
host all pgbouncer 203.0.113.12/32 md5

Create replication user

Create a dedicated PostgreSQL user for replication and a user for PgBouncer connections.

sudo -u postgres psql -c "CREATE USER replicator REPLICATION LOGIN ENCRYPTED PASSWORD 'SecureReplPassword123!';"
sudo -u postgres psql -c "CREATE USER pgbouncer LOGIN ENCRYPTED PASSWORD 'SecurePgBouncerPass123!';"

Restart primary PostgreSQL

Restart PostgreSQL on the primary server to apply the replication configuration changes.

sudo systemctl restart postgresql
sudo systemctl restart postgresql-17

Create base backup for replica servers

Use pg_basebackup to create an initial copy of the primary database for each replica server. Run this on each replica server.

sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17/main/*
sudo -u postgres pg_basebackup -h 203.0.113.10 -D /var/lib/postgresql/17/main -U replicator -P -v -R -W -C -S replica_slot_1
Note: Replace 203.0.113.10 with your primary server IP. Use different slot names (replica_slot_2, etc.) for additional replicas.

Configure replica servers

Edit the PostgreSQL configuration on replica servers to optimize for read-only workloads and enable hot standby.

listen_addresses = '*'
hot_standby = on
max_standby_archive_delay = 30s
max_standby_streaming_delay = 30s
wal_receiver_status_interval = 2s
hot_standby_feedback = on

Start replica servers

Start PostgreSQL on all replica servers. They will automatically begin streaming replication from the primary.

sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo systemctl start postgresql-17
sudo systemctl enable postgresql-17

Install PgBouncer on connection pooling server

Install PgBouncer on a dedicated server that will handle connection pooling and load balancing between your PostgreSQL servers.

sudo apt update
sudo apt install -y pgbouncer
sudo dnf install -y pgbouncer

Configure PgBouncer main settings

Configure PgBouncer with connection pooling settings and define your PostgreSQL servers for load balancing.

[databases]
; Primary server for writes
myapp_write = host=203.0.113.10 port=5432 dbname=myapp
; Replica servers for reads
myapp_read = host=203.0.113.11 port=5432 dbname=myapp
myapp_read_2 = host=203.0.113.12 port=5432 dbname=myapp

[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
admin_users = pgbouncer
stats_users = pgbouncer
pool_mode = transaction
server_reset_query = DISCARD ALL
max_client_conn = 1000
default_pool_size = 100
server_lifetime = 3600
server_idle_timeout = 600
client_idle_timeout = 600
server_connect_timeout = 15
query_timeout = 0

Configure PgBouncer authentication

Create the user authentication file for PgBouncer with encrypted passwords.

echo '"pgbouncer" "md5' $(echo -n 'SecurePgBouncerPass123!pgbouncer' | md5sum | cut -d' ' -f1) '"' | sudo tee /etc/pgbouncer/userlist.txt
echo '"myapp_user" "md5' $(echo -n 'MyAppPassword123!myapp_user' | md5sum | cut -d' ' -f1) '"' | sudo tee -a /etc/pgbouncer/userlist.txt

Set PgBouncer file permissions

Set secure permissions on PgBouncer configuration files to protect database credentials.

sudo chown pgbouncer:pgbouncer /etc/pgbouncer/pgbouncer.ini /etc/pgbouncer/userlist.txt
sudo chmod 640 /etc/pgbouncer/pgbouncer.ini /etc/pgbouncer/userlist.txt
sudo mkdir -p /var/log/pgbouncer
sudo chown pgbouncer:pgbouncer /var/log/pgbouncer

Create application database and user

Create your application database and user on the primary PostgreSQL server. This will automatically replicate to all replica servers.

sudo -u postgres createdb myapp
sudo -u postgres psql -c "CREATE USER myapp_user WITH ENCRYPTED PASSWORD 'MyAppPassword123!';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;"

Configure systemd for PgBouncer

Create a systemd service file for PgBouncer to ensure it starts automatically and runs with proper user permissions.

[Unit]
Description=PgBouncer PostgreSQL connection pooler
Documentation=https://pgbouncer.github.io/
After=network.target

[Service]
Type=notify
User=pgbouncer
Group=pgbouncer
ExecStart=/usr/bin/pgbouncer /etc/pgbouncer/pgbouncer.ini
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed
KillSignal=SIGINT
TimeoutSec=120

[Install]
WantedBy=multi-user.target

Start and enable PgBouncer

Start PgBouncer and enable it to start automatically on boot.

sudo systemctl daemon-reload
sudo systemctl enable pgbouncer
sudo systemctl start pgbouncer
sudo systemctl status pgbouncer

Configure load balancing script

Create a simple script to implement read/write splitting by connecting to different PgBouncer databases based on query type.

#!/bin/bash

Simple PostgreSQL load balancing script

Usage: pg_loadbalancer.sh [read|write] "SQL_COMMAND"

MODE=$1 QUERY=$2 PGBOUNCER_HOST="203.0.113.13" # Replace with your PgBouncer server IP PGBOUNCER_PORT="6432" USERNAME="myapp_user" PASSWORD="MyAppPassword123!" if [ "$MODE" = "write" ]; then DATABASE="myapp_write" else # Round-robin between read replicas REPLICA_NUM=$((RANDOM % 2 + 1)) if [ $REPLICA_NUM -eq 1 ]; then DATABASE="myapp_read" else DATABASE="myapp_read_2" fi fi PGPASSWORD="$PASSWORD" psql -h "$PGBOUNCER_HOST" -p "$PGBOUNCER_PORT" -U "$USERNAME" -d "$DATABASE" -c "$QUERY"
sudo chmod +x /usr/local/bin/pg_loadbalancer.sh

Configure connection monitoring

Set up a monitoring script to check the health of your PostgreSQL servers and automatically adjust PgBouncer configuration if servers become unavailable.

#!/bin/bash

PostgreSQL cluster health monitoring

PRIMARY_HOST="203.0.113.10" REPLICA1_HOST="203.0.113.11" REPLICA2_HOST="203.0.113.12" PGBOUNCER_HOST="203.0.113.13" MONITOR_USER="pgbouncer" MONITOR_PASS="SecurePgBouncerPass123!" check_server() { local host=$1 PGPASSWORD="$MONITOR_PASS" psql -h "$host" -U "$MONITOR_USER" -d postgres -c "SELECT 1;" &>/dev/null return $? }

Check primary server

if check_server "$PRIMARY_HOST"; then echo "$(date): Primary server ($PRIMARY_HOST) is healthy" else echo "$(date): WARNING: Primary server ($PRIMARY_HOST) is down!" # Log to syslog for alerting logger -t pg_monitor "CRITICAL: PostgreSQL primary server is down" fi

Check replica servers

for replica in "$REPLICA1_HOST" "$REPLICA2_HOST"; do if check_server "$replica"; then echo "$(date): Replica server ($replica) is healthy" else echo "$(date): WARNING: Replica server ($replica) is down!" logger -t pg_monitor "WARNING: PostgreSQL replica server $replica is down" fi done
sudo chmod +x /usr/local/bin/pg_health_monitor.sh

Schedule health monitoring

Add the health monitoring script to cron to run every 5 minutes and log results.

sudo crontab -e

Add this line to the crontab:

/5    * /usr/local/bin/pg_health_monitor.sh >> /var/log/postgresql/health_monitor.log 2>&1

Verify your setup

Test the streaming replication and PgBouncer connection pooling to ensure everything is working correctly.

# Check replication status on primary server
sudo -u postgres psql -c "SELECT client_addr, state, sync_state FROM pg_stat_replication;"

Check replication lag on replica servers

sudo -u postgres psql -c "SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS lag_seconds;"

Test PgBouncer connections

PGPASSWORD="MyAppPassword123!" psql -h 203.0.113.13 -p 6432 -U myapp_user -d myapp_write -c "SELECT version();" PGPASSWORD="MyAppPassword123!" psql -h 203.0.113.13 -p 6432 -U myapp_user -d myapp_read -c "SELECT version();"

Check PgBouncer statistics

PGPASSWORD="SecurePgBouncerPass123!" psql -h 203.0.113.13 -p 6432 -U pgbouncer -d pgbouncer -c "SHOW POOLS;" PGPASSWORD="SecurePgBouncerPass123!" psql -h 203.0.113.13 -p 6432 -U pgbouncer -d pgbouncer -c "SHOW STATS;"

Test load balancing script

/usr/local/bin/pg_loadbalancer.sh write "CREATE TABLE test_table (id SERIAL PRIMARY KEY, name VARCHAR(100));" /usr/local/bin/pg_loadbalancer.sh read "SELECT * FROM test_table;"

Run health monitor manually

/usr/local/bin/pg_health_monitor.sh
Note: Replace IP addresses in the commands with your actual server IPs. The replication lag should be very low (under 1 second) for healthy streaming replication.

Common issues

SymptomCauseFix
Replica can't connect to primarypg_hba.conf missing replication entryAdd host replication replicator IP/32 md5 to primary
PgBouncer authentication failsUser not in userlist.txt or wrong password hashRecreate userlist.txt with correct MD5 hash
Connection refused to PgBouncerFirewall blocking port 6432sudo ufw allow 6432 or configure iptables
High replication lagNetwork issues or heavy write loadCheck wal_keep_size and network bandwidth
PgBouncer shows no serversPostgreSQL servers unreachableCheck PostgreSQL status and network connectivity
Replica shows as async when expecting syncsynchronous_standby_names not configuredSet synchronous_standby_names = 'replica_slot_1' on primary

Next steps

Running this in production?

Want this handled for you? Running this at scale adds a second layer of work: capacity planning, failover drills, backup verification, and on-call response when replication breaks. See how we run infrastructure like this for European SaaS and fintech teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle high availability infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.