Architecture and operations
David Sterling  

PostgreSQL High Availability: Patroni & Automatic Failover

A three-node Patroni cluster on commodity VMs costs less than one hour of downtime. Here is the architecture, the config, and how to prove it works before you need it.

Tom Lane recently walked through the architectural decisions that shaped 30 years of Postgres, and one theme runs through it: Postgres was built to be simple, dependable, and boring. That is exactly why it is so good at high availability — and why so many teams still run a single node and hope.

The reference HA stack for 2026: Patroni for orchestration, etcd for consensus, streaming replication for data, and HAProxy for the connection endpoint.

Why Single-Node Postgres Is a Business Risk

Availability is two numbers. Recovery Point Objective (RPO) is how much data you can lose; Recovery Time Objective (RTO) is how long you can be down. A single node with nightly pg_dump gives you an RPO of up to 24 hours and an RTO in hours. That is not fine for a Houston energy trading desk, a clinic scheduling system, or a port-logistics pipeline where a stopped database stops revenue.

HA means removing every single point of failure: the database process, the server, and — the one teams forget — the address your application connects to. If your app hard-codes the primary’s IP, a failover does not help you. That is why the connection endpoint sits in front of the cluster.

The Reference Three-Node Stack

        +---------------------+
 app --> |  HAProxy :5000      |  (writes -> primary)
        |  HAProxy :5001      |  (reads  -> replicas)
        +----------+----------+
                   |
      +------------+------------+
      |            |            |
  pg-node-1    pg-node-2    pg-node-3
  PostgreSQL   PostgreSQL   PostgreSQL
  Patroni      Patroni      Patroni
      |            |            |
      +------------+------------+
                   |
        etcd (3 nodes, quorum = 2)

Patroni runs on every node and holds a leader lease in etcd. Exactly one node may be primary. If the leader misses its lease TTL, the others hold an election and one promotes itself. The etcd quorum prevents split-brain: with three nodes you can lose one and still fail over, but a node cut off from the quorum demotes itself rather than run as a rogue primary.

Step 1: Prepare Postgres for Replication

Streaming replication settings go in postgresql.conf on all three nodes:

wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
wal_keep_size = 1GB

# synchronous replication (see the tradeoff below)
synchronous_commit = on
synchronous_standby_names = 'ANY 1 (pg-node-2, pg-node-3)'

Then allow the replication user to connect from the cluster subnet in pg_hba.conf:

host  replication  replicator  10.10.0.0/24  scram-sha-256
host  all          all         10.10.0.0/24  scram-sha-256

Step 2: Configure Patroni and etcd

Install a three-node etcd cluster first — it holds the cluster state. Then drop a patroni.yml on each node, changing only name, connect_address, and the paths. Full options are in the Patroni documentation.

scope: htx-pg-cluster
namespace: /service/
name: pg-node-1

restapi:
  listen: 0.0.0.0:8008
  connect_address: 10.10.0.11:8008

etcd3:
  hosts: 10.10.0.11:2379,10.10.0.12:2379,10.10.0.13:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576   # 1 MB
    synchronous_mode: true
  initdb:
    - encoding: UTF8
    - data-checksums

postgresql:
  listen: 0.0.0.0:5432
  connect_address: 10.10.0.11:5432
  data_dir: /var/lib/postgresql/18/data
  bin_dir: /usr/lib/postgresql/18/bin
  authentication:
    replication:
      username: replicator
      password: REPLICATION_PASSWORD
    superuser:
      username: postgres
      password: POSTGRES_PASSWORD
  parameters:
    wal_level: replica
    hot_standby: on
    max_wal_senders: 10
    max_replication_slots: 10

Launch patroni /etc/patroni.yml on all three nodes. Patroni runs pg_basebackup for the replicas, creates the replication slots, and rewrites postgresql.conf and pg_hba.conf as cluster state changes. Hand-edit those files no more — Patroni owns them.

Step 3: Put HAProxy in Front

HAProxy polls Patroni’s REST API and routes traffic to whichever node reports the right role. Patroni returns HTTP 200 on /primary only for the leader, and 200 on /replica for followers.

listen postgres_primary
    bind *:5000
    option httpchk GET /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server pg-node-1 10.10.0.11:5432 check port 8008
    server pg-node-2 10.10.0.12:5432 check port 8008
    server pg-node-3 10.10.0.13:5432 check port 8008

Add a second listener on port 5001 with httpchk GET /replica and balance roundrobin for read traffic. Point the app at the primary endpoint for writes and the replica endpoint for reporting. Because the endpoints never change, failover is invisible to the app beyond the reconnect.

Synchronous vs. Asynchronous: Pick Your RPO

Asynchronous replication is fast and simple but can lose the last few transactions on failover. Synchronous replication guarantees zero data loss, but every commit waits for a standby, so write latency is now the slowest of the required replicas. ANY 1 tolerates one standby being down; set synchronous_mode_strict: true in Patroni if you want writes to block rather than silently fall back to async.

For most Houston workloads the right answer is synchronous replication with one required standby, placed in a second availability zone. You pay a millisecond or two per commit. You avoid explaining a lost ledger row to an auditor.

Failover: What Actually Happens

When the primary dies, Patroni on the survivors sees the etcd lease expire (worst case, roughly the 30-second ttl). They elect a new leader, the winner promotes itself, and clients reconnect through HAProxy. Lagging replicas are re-cloned or caught up automatically. The key guardrail: a node that loses etcd connectivity stops accepting writes and demotes, which is what keeps you from ending up with two primaries.

Test It Before You Need It

An untested failover plan is a wish. Run these on a schedule:

patronictl -c /etc/patroni.yml list                # who is leader, who lags
patronictl -c /etc/patroni.yml switchover --force  # planned move
patronictl -c /etc/patroni.yml failover --candidate pg-node-2
systemctl stop patroni                             # kill the leader, time it

Time each run and write the number down. If a failover takes 45 seconds, your runbook should say 45 seconds, not “a few minutes.”

What to Monitor

Three signals catch almost every failure early: replication lag, the current leader’s identity, and whether the endpoints still have healthy members.

SELECT client_addr, state, sync_state, replay_lag
FROM pg_stat_replication;

-- Patroni REST health (200 = healthy for that role)
curl -s http://10.10.0.11:8008/primary -o /dev/null -w '%{http_code}\n'

Alert on rising lag, on nodes flapping between roles, and on the survivor count dropping to one. A single healthy replica is not redundancy — it is a countdown.

When Managed HA Is the Better Call

Patroni costs you etcd nodes, HAProxy nodes, and a runbook. If your team is small, Amazon RDS Multi-AZ or Aurora gives you automatic failover with less operational surface — at the cost of control over versions, extensions, and failover timing. The rule: if you already run your own Postgres for extension or tuning reasons, self-managed Patroni earns its keep. If you are happy on RDS, stay there and spend the effort on failure testing and backups.

The Short Version

  • HA = database + server + connection endpoint. Fix all three or none.
  • Three Patroni nodes with a three-node etcd quorum survive one failure and prevent split-brain.
  • Route writes through HAProxy /primary and reads through /replica; never hard-code the primary’s IP.
  • Synchronous replication with one required standby buys zero data loss for a millisecond of write latency.
  • Test the failover, time it, and write the number into the runbook.

Postgres has survived 30 years because it stays boring and dependable. Your production topology should be boring too — and it should already have failed over once in a game-day drill before the real thing.

Leave A Comment