Originally published on the HostingPodium Enterprise Engineering Blog.
Free SRE & Developer Diagnostics: Test your stack live with our Global DNS Propagation Checker & SSL Health Auditor, Free Website Speed & Core Web Vitals Checker, or explore all 9 Free Webmaster & Cloud Engineering Tools.
This guide explains how to construct a zero-data-loss PostgreSQL HA cluster with Patroni, etcd, and HAProxy across three Linux zones, covering configuration, kernel tuning, and operational runbooks.
Scope
This guide details a production-grade PostgreSQL high-availability (HA) stack built on Patroni, etcd, and HAProxy. The target environment is a three-zone deployment on RHEL 9 or AlmaLinux 9, each zone hosting a dedicated compute node. The design guarantees zero data loss under a single-zone outage, enforces split-brain protection at the consensus layer, and provides deterministic client routing via health-checked TCP load balancing.
Cluster topology
The logical layout consists of:
| Component | Instances per zone | Purpose |
|---|---|---|
| etcd | 1 | Consensus store for Patroni metadata |
| Patroni + PostgreSQL | 1 | Primary or replica, runs synchronous replication |
| HAProxy | 1 (optional active-passive pair) | Client entry point, performs TCP health checks |
All nodes share a private VLAN (10.0.0.0/24) for inter-process traffic and a public VLAN for client access. The HAProxy virtual IP (VIP) is allocated from the public subnet and is moved by keepalived on failover.
etcd consensus layer
etcd runs as a three-member cluster, one member per zone. The --initial-cluster-state flag is set to new on first boot, and --initial-cluster-token is a UUID unique to the deployment. To prevent split-brain, the --auto-compaction-retention is set to 1h and the election timeout is tuned to 5 seconds.
# /etc/etcd/etcd.conf
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_NAME="etcd-$(hostname)"
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://$(hostname -I | awk '{print $1}'):2380"
ETCD_ADVERTISE_CLIENT_URLS="https://$(hostname -I | awk '{print $1}'):2379"
ETCD_INITIAL_CLUSTER="etcd-node1=https://10.0.0.11:2380,etcd-node2=https://10.0.0.12:2380,etcd-node3=https://10.0.0.13:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="$(cat /etc/etcd/token)"
ETCD_CLIENT_CERT_AUTH="true"
ETCD_PEER_CLIENT_CERT_AUTH="true"
ETCD_CERT_FILE="/etc/etcd/ssl/etcd.pem"
ETCD_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"
ETCD_TRUSTED_CA_FILE="/etc/etcd/ssl/ca.pem"
ETCD_QUOTA_BACKEND_BYTES=8589934592 # 8 GiB
ETCD_ELECTION_TIMEOUT=5000
ETCD_HEARTBEAT_INTERVAL=250
ETCD_AUTO_COMPACTION_RETENTION=1h
All etcd traffic is encrypted with TLS 1.2+. The quorum size of three ensures that any two zones can form a majority, satisfying the majority rule for Raft.
Patroni configuration
Patroni reads its state from etcd and controls PostgreSQL via pg_ctl. The configuration file is identical on each node, with zone-specific values injected through environment variables.
# /etc/patroni/patroni.yml
scope: pg-ha-cluster
namespace: /db/
name: $(hostname)
restapi:
listen: 0.0.0.0:8008
connect_address: $(hostname -I | awk '{print $1}'):8008
etcd:
host: https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379
protocol: https
cacert: /etc/patroni/ssl/ca.pem
cert: /etc/patroni/ssl/patroni.pem
key: /etc/patroni/ssl/patroni-key.pem
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1 MiB
postgresql:
use_pg_rewind: true
parameters:
wal_level: replica
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 1024 # MiB
synchronous_commit: on
synchronous_standby_names: "*"
hot_standby: on
max_connections: 500
shared_buffers: 4GB
effective_cache_size: 12GB
work_mem: 64MB
maintenance_work_mem: 512MB
checkpoint_timeout: 5min
checkpoint_completion_target: 0.9
initdb:
- encoding: UTF8
- locale: en_US.UTF-8
- data-checksums
pg_hba:
- host all all 10.0.0.0/24 md5
- host replication all 10.0.0.0/24 md5
- host all all 0.0.0.0/0 reject
users:
admin:
password: "$(cat /etc/patroni/secrets/admin.pass)"
options: [CREATEDB]
postgresql:
listen: 0.0.0.0:5432
data_dir: /var/lib/pgsql/13/data
bin_dir: /usr/pgsql-13/bin
authentication:
replication:
username: replicator
password: "$(cat /etc/patroni/secrets/replicator.pass)"
superuser:
username: postgres
password: "$(cat /etc/patroni/secrets/postgres.pass)"
Key Patroni Bootstrap & Synchronous Replication Parameters
synchronous_standby_names="*"forces every replica to acknowledge writes, guaranteeing zero data loss as long as a quorum of replicas remains reachable.maximum_lag_on_failover=1MiBprevents a lagging replica from being promoted.wal_keep_size=1024reserves enough WAL for a full-zone outage.
HAProxy routing
HAProxy runs in TCP mode on a dedicated VIP (10.0.1.100). It performs a health check against the Patroni REST API; only nodes reporting state: "running" and role: "master" are advertised as primary. Replicas are exposed on a separate port for read-only workloads.
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 5000
daemon
tune.ssl.default-dh-param 2048
ssl-default-bind-ciphers PROFILE=SYSTEM
ssl-default-server-ciphers PROFILE=SYSTEM
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
nbproc 2
cpu-map 1 0-3
cpu-map 2 4-7
defaults
mode tcp
log global
timeout connect 5s
timeout client 30s
timeout server 30s
option tcplog
retries 3
frontend pgsql_front
bind 10.0.1.100:5432
default_backend pgsql_primary
acl is_read path_beg /read
use_backend pgsql_replica if is_read
backend pgsql_primary
option httpchk GET /master
http-check expect status 200
server pg1 10.0.0.21:5432 check port 8008 inter 2s rise 2 fall 3
server pg2 10.0.0.22:5432 check port 8008 inter 2s rise 2 fall 3 backup
server pg3 10.0.0.23:5432 check port 8008 inter 2s rise 2 fall 3 backup
backend pgsql_replica
option httpchk GET /replica
http-check expect status 200
server pg1 10.0.0.21:5432 check port 8008 inter 2s rise 2 fall 3 disabled
server pg2 10.0.0.22:5432 check port 8008 inter 2s rise 2 fall 3
server pg3 10.0.0.23:5432 check port 8008 inter 2s rise 2 fall 3
Failover is driven by keepalived moving the VIP to the node whose Patroni instance reports role: "master". The health-check interval of 2 seconds yields sub-second detection of primary loss.
Kernel and system parameters
Linux kernel tuning focuses on network buffers, shared memory limits, and file descriptor counts. The following sysctl settings are applied on every PostgreSQL node:
| Parameter | Value | Rationale |
|---|---|---|
| net.core.somaxconn | 4096 | Allows HAProxy to accept many concurrent connections. |
| net.ipv4.tcp_tw_reuse | 1 | Reduces TIME_WAIT buildup under heavy client churn. |
| vm.max_map_count | 262144 | Required for large shared memory segments used by PostgreSQL. |
| kernel.shmmax | 68719476736 | 68 GiB – matches shared_buffers plus overhead. |
| kernel.shmall | 16777216 | Number of pages (8 KiB each) to support shmmax. |
| fs.file-max | 1000000 | Ensures PostgreSQL can open the required number of sockets and files. |
Persist the values in /etc/sysctl.d/99-pg-ha.conf and reload with sysctl --system.
Synchronous replication strategy
Patroni is configured for synchronous_commit=on and synchronous_standby_names="*". In a three-zone deployment this means the primary must receive acknowledgment from both replicas before confirming a transaction. The write latency typically rises by 2-4 ms, measured with pgbench -c 50 -j 4 -T 60. The trade-off is acceptable for workloads that cannot tolerate data loss.
In the event that a replica becomes unreachable, Patroni automatically demotes the primary to asynchronous mode after a configurable failover_timeout (default 30 seconds). This prevents a permanent write stall while preserving data integrity once the missing replica rejoins.
Failover and recovery runbook
Detect primary loss: HAProxy health check fails, keepalived triggers VIP move.
Patroni election: Remaining nodes consult etcd; the node with the lowest
namewins the Raft election and assumesrole: "master".Promote replica: Patroni runs
pg_ctl promote. PostgreSQL replays pending WAL up to the last synchronous commit.Validate quorum: Verify
SELECT pg_is_in_recovery();returns false on the new primary andSELECT * FROM pg_stat_replication;shows both replicas in sync.Update HAProxy: Health check endpoint
/masternow returns 200, HAProxy routes traffic to the new primary within two seconds.Re-integrate failed zone: Once the node is back online, start etcd, let it join the Raft cluster, then start Patroni. It will register as a replica and begin streaming WAL.
Post-failover audit: Capture
etcdctl endpoint status --write-out=tableandpatronictl listoutput for compliance records.
If the failed zone contained the original primary and its data directory is corrupted, initiate a fresh base backup using pg_basebackup -D /var/lib/pgsql/13/data -Fp -Xs -P -R from the current primary before re-joining.
Observability stack
Metrics are collected via Prometheus exporters:
etcd exporter on port 2379/metrics
Patroni exporter (built-in) on port 8008/metrics
PostgreSQL exporter on port 9187
HAProxy exporter on port 9101
Alertmanager Rules & Quorum Loss Thresholds
# alerts.yml
- alert: PostgreSQLPrimaryDown
expr: patroni_role{role="master"} == 0
for: 30s
labels:
severity: critical
annotations:
summary: "Primary PostgreSQL instance is down"
runbook: "https://internal.runbooks/pg-primary-failure"
- alert: EtcdQuorumLost
expr: etcd_server_has_leader == 0
for: 15s
labels:
severity: warning
annotations:
summary: "Etcd cluster has lost quorum"
runbook: "https://internal.runbooks/etcd-quorum"
Logs are shipped to Elasticsearch via Filebeat; each component tags logs with service=postgresql|etcd|haproxy for easy correlation.
Trade-offs
Latency vs. durability: Synchronous replication adds 2-4 ms per transaction; asynchronous mode reduces latency but permits data loss if the primary crashes.
Complexity of etcd: Managing a Raft cluster adds operational overhead compared to a simple file-based DCS, but it eliminates split-brain scenarios that can arise with ZooKeeper or Consul under network partitions.
HAProxy vs. PgBouncer: HAProxy offers TCP-level health checks and VIP failover, whereas PgBouncer provides connection pooling but cannot move a VIP without additional tooling.
Resource footprint: Running three full PostgreSQL instances with 4 GiB shared buffers each consumes ~12 GiB RAM per node; sizing must account for OS, etcd, and HAProxy overhead.
Operational checklist
Validate TLS certificates on etcd, Patroni, and HAProxy before deployment.
Run
etcdctl endpoint healthon each member; expect 100 % success.Confirm
patronictl listshows one master and two replicas.Execute
pgbench -c 10 -j 2 -T 30and record 95th-percentile latency; must stay below 10 ms under baseline load.Test failover by stopping
patronion the master; verify VIP moves and new master promotion within 30 seconds.Inspect Prometheus alerts after each test; no pending critical alerts should remain.
Back up etcd snapshots daily with
etcdctl snapshot save /backups/etcd-$(date +%F).db.
Following this checklist on a weekly cadence keeps the HA stack within SLA limits and provides early detection of configuration drift.
Summary
The combination of Patroni, etcd, and HAProxy delivers a deterministic failover path, strict split-brain avoidance, and zero-data-loss guarantees for PostgreSQL workloads spread across three availability zones. By adhering to the kernel parameters, replication settings, and runbook steps outlined above, operators can meet sub-second recovery objectives and maintain consistent performance under load.
Free Webmaster, SRE & Cloud Diagnostic Tools (No Signup Required)
Verify and benchmark your production infrastructure directly from your browser using the HostingPodium Free Webmaster & Developer Suite:
| Free Engineering Utility | Primary Diagnostic Use Case |
|---|---|
| Global DNS Propagation & SSL Checker | Verify live A, AAAA, CNAME, MX, TXT, NS & CAA records across 21+ global resolvers + TLS certificate audits |
| Website Speed Test & Core Web Vitals Auditor | Measure server TTFB, LCP, INP, CLS, and HTTP/2 compression latency in real time |
| Rich Snippets & Schema.org JSON-LD Generator | Generate & validate structured JSON-LD markup for TechArticles, FAQs, Products & Organizations |
| HTTP Redirect Chain & Status Code Tracer | Trace 301/302 redirect hops, inspect response headers, and verify load balancer health checks |
| DMARC, SPF & DKIM Email Security Validator | Audit domain email authentication policies and generate compliant DMARC records |
| Google SERP Simulator & Meta Tag Previewer | Preview desktop/mobile Google snippets, OpenGraph cards, and pixel-width truncation |
| Free WebP Image Converter & Optimizer | Client-side PNG/JPG to WebP compression for faster Core Web Vitals LCP |
Related Enterprise Architecture & Benchmarks
- Canonical Engineering Guide: Designing PostgreSQL HA with Patroni, etcd, and HAProxy in Multi-Zone Linux Clusters
- Managed Engineering Practice: 24/7 Managed Infrastructure, Linux SRE & Observability
- Infrastructure Benchmark Comparison: Hetzner Alternative: 24/7 Managed Bare Metal vs Unmanaged Hardware
Top comments (0)