When people first start deploying PostgreSQL on Kubernetes, they usually focus on StatefulSets, persistent volumes, or backup strategies. But once you start thinking about real-world failures, a much harder question comes up: what happens if the primary node crashes? Who decides which node should be promoted to the new primary, and how can that transition happen safely without causing split-brain? That is exactly where Patroni becomes worth understanding in depth, not just at the level of knowing how to use it, but at the level of knowing why it works.

In this article, I’ll walk you through how Patroni implements high availability for PostgreSQL on Kubernetes in a way that is easy to follow while staying close to the actual internal mechanics. The goal is not just to help you get Patroni running, but to give you a solid understanding of core concepts such as leader locks, the health check loop, leader races, and failsafe mode.

The problem

Imagine this: it is 2 a.m., and the primary node in your PostgreSQL cluster suddenly crashes. Applications start reporting connection errors. Alerts begin firing. The question is: who, or what, will elect a new primary before users even notice something is wrong?

PostgreSQL does not provide built-in high availability. If the primary node crashes, the entire cluster becomes read-only or stops serving traffic altogether until a DBA intervenes manually. On Kubernetes, the problem becomes even more complicated: pods can be rescheduled at any time, IP addresses can change, and there is not always a DBA available to respond within the first few seconds of an incident.

This is exactly the problem Patroni was designed to solve. Patroni is a high-availability solution for PostgreSQL across multiple runtime environments, using automatic failover to keep the PostgreSQL cluster available even when the primary node fails. This article explains how Patroni works internally on Kubernetes, from periodic health checks and leader locks to the mechanisms it uses to avoid race conditions.

What you will learn

By the end of this article, you will understand:

  • How Patroni structures its health check loop to keep the cluster operating normally.
  • How the leader race works, including the conditions a replica must meet to be promoted and how the election process unfolds.
  • How Patroni uses Kubernetes resourceVersion to prevent split-brain.
  • How failsafe mode works and what its limitations are when the DCS is unavailable.
  • Patroni’s strengths, limitations, and when it is—or is not—the right choice.

Key terms

PostgreSQL cluster: A group of nodes deployed in a 1 primary to N replicas architecture.

PostgreSQL cluster architecture with 1 primary and N replicas

PostgreSQL cluster architecture with 1 primary and N replicas

Race condition: A situation in which multiple processes attempt to modify the same shared resource at the same time, leading to unexpected results.

resourceVersion: Kubernetes uses resourceVersion to implement optimistic concurrency. When a resource is updated, Kubernetes checks whether the resourceVersion in the request matches the current resourceVersion of that resource.

  • If they match, the update is accepted and resourceVersion is advanced to a new value.
  • If they do not match, Kubernetes rejects the update and returns HTTP 409.

For example, suppose there is a Service resource with resourceVersion 308. You attempt to update that Service, but the request carries an outdated resourceVersion value of 308 after someone else has already modified it. As a result, the request receives HTTP 409 and the update fails.

Patroni architecture and operating model

1. High-level architecture

Patroni uses a DCS (Distributed Configuration Store) as the source of truth for storing cluster metadata. Patroni supports multiple DCS backends, including etcd, Consul, ZooKeeper, and Kubernetes ConfigMaps or Endpoints.

Patroni is deployed alongside PostgreSQL, with each PostgreSQL node running its own Patroni instance. A Patroni cluster mirrors the topology of the PostgreSQL cluster: one primary and N replicas. The Patroni primary holds a leader lock stored in the DCS, while the Patroni replicas continuously monitor that lock.

The role of each Patroni instance is determined through the DCS and directly controls the role of its corresponding PostgreSQL instance: whichever Patroni instance holds the leader lock promotes its PostgreSQL instance to primary, while the others remain replicas.

Patroni deployment model

Patroni deployment model

2. The health check loop

Patroni supports two mechanisms for storing the leader lock on Kubernetes: Endpoints and ConfigMaps. This article focuses on Kubernetes Endpoints acting as the DCS. When Endpoints are used as the DCS, the leader lock is stored like this:

$ kubectl get ep postgres-cluster -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    leader: postgres-cluster-0
    renewTime: "2026-05-10T09:03:36.094563+00:00"
    ttl: "30"
  name: postgres-cluster
  resourceVersion: "49818310"
...

The leader lock is effectively a Kubernetes Endpoints resource, where the renewTime annotation records the last time the Patroni primary refreshed the lock. The leader lock is associated with a TTL (time to live).

Patroni checks the cluster state on a regular loop (every 10 seconds by default, configured through the loop_wait parameter). In each iteration, Patroni performs the following steps in order:

  1. Read the current state from the DCS.
  2. Check the leader lock status:
    1. If the lock is still valid, the primary renews it and the replicas take no action on the lock.
    2. If the lock has expired or does not exist, all replicas begin competing for a new lock—this is the leader race.

Example: The lock is renewed at 11:30:00 with a TTL of 30 seconds. If it has not been renewed by 11:30:30, the replicas begin a leader race.

3. Automatic failover

When the leader lock expires:

  1. Each Patroni replica checks whether it is eligible to become the new leader based on three conditions:
    1. Lag: Replication lag must not exceed maximum_lag_on_failover.
    2. Timeline: The node must be on a WAL timeline that is compatible with the cluster.
    3. Quorum (if synchronous mode is enabled): The node must be in the list of synchronous standbys, meaning it has received and written all required WAL records from the primary before the crash.
  2. The first Patroni replica to acquire the lock is promoted to Patroni primary. At that point, Patroni calls pg_promote() to promote the corresponding PostgreSQL instance. The remaining nodes detect the new leader and switch to following that node’s lock.

At the same time, if the old Patroni primary comes back after the failure and discovers that a new leader has already been elected, Patroni automatically demotes it: PostgreSQL is stopped, pg_rewind is run to realign its WAL timeline with the new primary (if enabled), and the node is then started again in standby mode.

So how does Patroni make sure there is no leader race conflict among replicas?

Patroni relies on Kubernetes resourceVersion to ensure that race conditions do not occur during leader election. Each node sends a lock update request together with the current resourceVersion of the Endpoint:

  • If the resourceVersion matches, the request succeeds and that node becomes the new leader.
  • If the resourceVersion does not match, Kubernetes returns HTTP 409 (StatusConflict), and the node cannot become the leader.

Case 1 — Multiple nodes compete for the lock: The current resourceVersion is 100. Two replicas simultaneously send requests to update the Endpoint with resourceVersion = 100. The first request succeeds, and that node is promoted to primary. The remaining requests receive HTTP 409 because the resourceVersion has already changed.

Sequence diagram for case 1

Sequence diagram for case 1

Case 2 — A slow node: The initial resourceVersion is 100. Node A updates successfully and becomes the leader, increasing the resourceVersion to 101. Node B is delayed; by the time it acts, it reads the latest state with resourceVersion = 101, sees that a valid leader already exists, and continues operating as a replica.

Sequence diagram for case 2

Sequence diagram for case 2

Case 3 — A delayed renewal request: If the primary’s lock renewal request is delayed long enough for the lock to expire, a leader race is triggered and a new node is elected, which changes the resourceVersion. When the old renewal request from the original primary finally arrives, Kubernetes rejects it with HTTP 409 because the resourceVersion no longer matches. The old primary realizes it has lost the lock and automatically transitions to the replica role.

Sequence diagram for case 3

Sequence diagram for case 3

4. Failsafe mode

Failsafe mode acts as a second layer of protection when the DCS becomes unavailable. When enabled, the primary can continue holding its role as long as it can still communicate with all other nodes in the cluster through Patroni’s REST API.

Sequence diagram showing how failsafe mode works in Patroni

Sequence diagram showing how failsafe mode works in Patroni

However, failover is no longer available in this state. If the primary fails while failsafe mode is active, no replica can be elected to replace it. The entire cluster becomes read-only.

What You Trade Off When Using Patroni for PostgreSQL HA on Kubernetes

Strengths:

  • Native Kubernetes integration, using Endpoints as the DCS without requiring a separate etcd cluster.
  • Fully automatic leader election.
  • Support for multiple HA options, including failsafe mode, synchronous replication, and custom bootstrap hooks.
  • A large community and extensive documentation.
  • Support for multiple DCS backends, including etcd, Consul, ZooKeeper, and Kubernetes Endpoints.

Limitations:

  • Dependency on the DCS: if the DCS fails, the cluster may become read-only unless failsafe mode is enabled.
  • Higher operational complexity compared with managed services such as RDS, Cloud SQL, or AlloyDB.
  • A solid understanding of parameters such as ttl, loop_wait, and maximum_lag_on_failover is required to tune Patroni correctly for a given workload.

Closing thoughts

If Patroni was previously just a name you often saw in PostgreSQL high-availability diagrams, I hope this article has made its internal behavior feel much more concrete. Rather than thinking of Patroni as simply an “automatic failover tool,” you can now see more clearly how it monitors cluster state, elects a new leader during failures, and works with Kubernetes to prevent dangerous situations such as split-brain. Once you understand these internal mechanics, you will be in a much better position not only to deploy Patroni, but also to decide whether it is truly the right fit for your Kubernetes environment.

At its core, Patroni solves the high-availability problem for PostgreSQL by continuously maintaining a leader lock in the DCS and automatically triggering an election when that lock expires. The core mechanisms work together across three layers:

  • The health check loop keeps the cluster state under continuous observation. The primary renews the lock, while replicas watch that lock to detect failures early.
  • The leader race begins when the lock expires. Eligible replicas compete to become the new primary based on factors such as replication lag, WAL timeline compatibility, and quorum. Patroni then relies on Kubernetes resourceVersion to ensure that only one node can acquire the lock, preventing split-brain.
  • Failsafe mode serves as a fallback layer when the DCS is unavailable. In this state, the primary can continue running as long as it can still communicate with the entire cluster, but the trade-off is that automatic failover is no longer possible.

More broadly, Patroni is a strong fit for systems that need a high degree of control and want to self-manage PostgreSQL on Kubernetes. For workloads where operational simplicity is the top priority, managed services remain a compelling alternative.