Spokes.wiki Search About
Created
Updated

Patroni

"Patroni is a Python template for building PostgreSQL high availability (HA) clusters." Started 2015-07-08 as a fork of Compose's Governor, MIT, Python, 8,670★ / 1,019 forks, 59 open issues, last pushed 2026-08-20, supporting PostgreSQL 9.3 through 18. The repository description names the dependency that turns out to be the whole design: "A template for PostgreSQL High Availability with Etcd, Consul, ZooKeeper, or Kubernetes."

Read from the repository and the project documentation (T1). It arrived through awesome-postgres' High-Availability section, but the reason it was pulled first is citus: that page recorded, as a negative finding, that a production distributed Postgres ships no HA of its own and defers to Patroni. This is the other half of that sentence.

A governance note the catalog does not carry: the project has moved out of its founder's namespace. zalando/patroni now redirects to patroni/patroni. zalando built it and the name is still attached to the Zalando Kubernetes operator; the code is no longer theirs alone.

Patroni does not implement consensus. It rents it.

This is the finding, and it is what the spoke has been missing since podc-lecture-notes gave it the theory. Patroni holds a leader lock — a key with a TTL — in an external distributed configuration store: etcd, Consul, ZooKeeper, or the Kubernetes API. The primary's job is to keep renewing that key. Everything else follows from whether it can.

The three numbers that govern it, quoted from the dynamic-configuration reference:

  • ttl (default 30s, minimum 20) — "the TTL to acquire the leader lock (in seconds). Think of it as the length of time before initiation of the automatic failover process."
  • loop_wait (default 10s, minimum 1) — "the number of seconds the loop will sleep."
  • retry_timeout (default 10s, minimum 3) — "timeout for DCS and PostgreSQL operation retries (in seconds). DCS or network issues shorter than this will not cause Patroni to demote the leader."

And the constraint binding them: loop_wait + 2 * retry_timeout <= ttl. That inequality is the entire availability/safety trade written as arithmetic. Every loop_wait seconds each node wakes, talks to the DCS and to its Postgres, and retries for up to retry_timeout when either is slow. The inequality guarantees a primary gets a full cycle plus two full retry budgets to renew before the key it is holding can expire under someone else. Tighten ttl and failover is faster but a brief network stall demotes a healthy primary; loosen it and the cluster is unavailable for longer while everyone waits out a dead one. Nothing here is subtle, and that is the point — the failover policy is three integers, because the hard part was delegated.

Who may take over is also policy, not algorithm. maximum_lag_on_failover is "the maximum bytes a follower may lag to be able to participate in leader election" — a durability floor expressed in bytes of WAL. With synchronous_mode on (off / on / quorum), the rule tightens: "only the last known leader, or one of synchronous replicas, are allowed to participate in leader race." And synchronous_mode_strict "prevents disabling synchronous replication if no synchronous replicas are available, blocking all client writes to the primary" — the CAP choice made explicit and configurable in one flag, availability given up to keep the durability promise.

Where Raft actually is in this stack

Growth edge 4 has been asking for Raft, "the algorithm most production stores actually run," and for an engine's own account of a real failover path. Patroni supplies the second and relocates the first. Raft is not in Postgres and not in Patroni's failover logic; it is in the store Patroni leans on. The repository's own topics list raft alongside etcd, consul and zookeeper, and the optional dependency list names a "python Raft DCS" for running without one.

(My reading, not the project's claim: the standard DCS options are themselves consensus systems — etcd and Consul run Raft, ZooKeeper runs Zab — so the quorum, the elections and the durable replicated log all live one layer down. Patroni's docs do not argue this; they simply treat the DCS as reliable and build on top.)

Which makes Patroni an almost exact instance of the argument in chubby-lock-service. Burrows' case for a lock service over a Paxos library was that a library needs a quorum of your servers while a lock service lets a single client proceed. Patroni is that: one Postgres primary, which is emphatically not a quorum member, made safe by holding a lease in somebody else's quorum. Eighteen years apart, the same shape — and the corpus can now point at the theory (paxos, consensus), the canonical production lock service (chubby-lock-service), and a thing that consumes one.

The watchdog, which is the part theory doesn't cover

Holding a lease keeps two primaries from being elected. It does not keep a demoted primary from still accepting commits — a stalled process, a slow shutdown, or a machine under load can leave Postgres serving writes after its lock is gone. The docs name the consequence:

"Having multiple PostgreSQL servers running as primary can result in transactions lost due to diverging timelines. This situation is also called a split-brain problem."

Patroni's first move is software: it "will try to achieve this by stopping PostgreSQL when leader lock update fails." The failsafe is hardware. A watchdog device "will reset the whole system when they do not get a keepalive heartbeat within a specified timeframe" — Patroni arms it to expire 5 seconds before the TTL does, so with the defaults (loop_wait=10, ttl=30) a node has "at least 15 seconds (ttl - safety_margin - loop_wait)" to shut down cleanly before the kernel reboots the machine out from under it. And the refusal that makes it real: "If watchdog activation fails and watchdog mode is required then the node will refuse to become leader."

That is fencing, and it is the gap between a consensus proof and a running cluster. The theory in podc-lecture-notes establishes that a majority can agree on who the leader is; it says nothing about persuading the loser to stop. Patroni's answer is to reboot it.

Why it belongs here, and what it doesn't answer

It is the spoke's first source on replication and failover as operated, the successor edge that postgresql and mysql both stop short of. It sits at the seam with ../platform-ops-wiki and stays here on the spoke's own boundary rule: the architecture of failover is a database-architecture subject, while running the resulting cluster on Kubernetes is that spoke's practice.

Not answered here. How Postgres streaming replication itself works — Patroni configures it and assumes it. What a failover costs in lost transactions under async replication beyond the maximum_lag_on_failover bound. And the leader-race loop's step-by-step order, which the project documents in an HA-loop diagram page that returned 404 at ingest time; the mechanism above is assembled from the configuration reference and the watchdog page, both first-party, and the gap is recorded rather than filled by inference.

The algorithm underneath (added 2026-08-22). The DCS options here — etcd, Consul, ZooKeeper — are Raft or Zab implementations, and raft-paper now explains the one Patroni's timing rules are shaped by: loop_wait + 2 * retry_timeout <= ttl is this spoke's operational form of Raft's own broadcastTime ≪ electionTimeout ≪ MTBF.

postgresql · citus · write-ahead-logging · chubby-lock-service · consensus · paxos · consistency-models · awesome-postgres · pgbackrest · operational-databases