Connection Proxies
During failover, something must send client connections to the new primary. HAProxy (or a cloud load balancer) routes TCP; PgBouncer 1.x pools sessions. They solve different layers.
Search across all documentation pages
During failover, something must send client connections to the new primary. HAProxy (or a cloud load balancer) routes TCP; PgBouncer 1.x pools sessions. They solve different layers.
HAProxy TCP frontends for write (primary) and read (replicas); PgBouncer in transaction pool mode for app workers.
# HAProxy backend checks Patroni REST (port 8008) for leader
# frontend pg_write -> backend patroni_primary (only leader passes check)
# frontend pg_read -> backend patroni_replicas (standbys pass replica check)# pgbouncer.ini snippet
[databases]
appdb = host=pg-write.internal port=5432 dbname=appdb
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 50
server_lifetime = 300When to reach for this: More than a handful of app servers connecting to PostgreSQL; failover must not require app config changes.
HAProxy Patroni check script and PgBouncer reload after failover:
#!/usr/bin/env bash
# patroni-primary-check.sh - exit 0 if this node is Patroni leader
LEADER=$(curl -sf http://127.0.0.1:8008/patroni | jq -r .role)
[[ "$LEADER" == "master" || "$LEADER" == "primary" ]]# haproxy.cfg excerpt
backend patroni_primary
mode tcp
option tcp-check
tcp-check connect port 8008
tcp-check send GET\ /primary\ HTTP/1.0\r\n
tcp-check expect string 200
server pg1 10.0.1.11:5432 check port 8008
server pg2 10.0.1.12:5432 check port 8008
server pg3 10.0.1.13:5432 check port 8008# After failover: bounce PgBouncer to drop stale server connections
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer -c "RECONNECT;"
# or: systemctl reload pgbouncerWhat this demonstrates:
/primary and /replica endpoints for health routing.pg-write.internal:6432 (PgBouncer) not individual node IPs.RECONNECT or reload clears pools pointing at old primary.| Component | OSI layer | Failover behavior |
|---|---|---|
| HAProxy / NLB | TCP (L4) | Routes to healthy Patroni leader |
| PgBouncer 1.x | Connection pool | Reopens backends after leader change |
| RDS Proxy | Managed pool + IAM | AWS handles backend swap |
| App JDBC URL | Client | Must use stable hostname + timeouts |
/replica check; tolerate slight lag for reporting.| Mode | HA note |
|---|---|
session | Safer for prepared statements; fewer connections saved |
transaction | Common for web apps; use DISCARD ALL compatibility |
statement | Rare; breaks many ORM patterns |
pool_mode = session for affected apps or disable prepared statements.server_lifetime - Stale primary connections for minutes. Fix: Lower lifetime; RECONNECT in Patroni on_role_change callback.| Alternative | Use When | Don't Use When |
|---|---|---|
| VIP + keepalived | Legacy datacenter | Cloud LB preferred |
| Kubernetes Service + Patroni endpoints | K8s-hosted PG | Bare-metal VMs |
| Direct DNS CNAME swap | Simplest small setups | Sub-minute RTO required |
Common: client -> PgBouncer -> HAProxy -> PostgreSQL, or client -> HAProxy -> PgBouncer -> PostgreSQL. Pick one topology and document it.
No. RDS Proxy pools connections; Aurora/RDS Multi-AZ handles failover underneath.
No. Streaming and logical replication connect directly to PostgreSQL port 5432.
Terminate at HAProxy or use sslmode=require end-to-end. Match compliance requirements.
At least two for HA; each points at same HAProxy write VIP.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 18, 2026