This guide covers running pgBully in production: monitoring, integrating leadership into your application, handling failover, the consistency caveats of a timeout-based algorithm, and troubleshooting.
The first stop is always pgbully.status():
SELECT * FROM pgbully.status();Useful derived checks:
-- Is this node the leader?
SELECT pgbully.is_leader();
-- Which nodes are currently unreachable from here?
SELECT node_id, last_seen
FROM pgbully.peers()
WHERE NOT reachable AND NOT is_self;
-- How stale is our view of the leader? (followers only)
SELECT heartbeat_age_ms FROM pgbully.status();| Condition | Query | Meaning |
|---|---|---|
| No leader | pgbully.leader() IS NULL for > a few election_timeouts |
cluster cannot converge |
| Heartbeat stale on a follower | heartbeat_age_ms > 3 × election_timeout |
leader may be gone / unreachable |
| Election churn | elections_started climbing steadily |
timeouts too tight, or flapping network |
| Peer down | peers().reachable = false persists |
investigate that node / network path |
If your monitoring already speaks the vendor-neutral interface, the same facts are available under those names — see cluster-api.md:
SELECT * FROM pgbully.get_cluster_status();
SELECT * FROM pgbully.member_list; -- one row per member, with status
SELECT * FROM pgbully.worker_status;pgbully.member_list is the quickest whole-cluster read: it marks each member
leader, follower or unavailable in one query, from any node.
pgBully logs state transitions at LOG level, e.g.:
LOG: pgbully: election worker started for node 3
LOG: pgbully: node 2 starting election
LOG: pgbully: node 3 won election, becoming leader (term 4)
LOG: pgbully: node 2 deferring to a higher-id peer
LOG: pgbully: node 5 stepping down; observed higher term 7
Grepping grep pgbully: $PGDATA/log/*.log gives a full election history.
For a stuck cluster, pgbully.set_debug(true) makes the worker log its state
on every pass:
LOG: pgbully: state=follower term=7 leader=3 heartbeat_age=214ms election=no
That is one line per loop iteration, so turn it off again with
pgbully.set_debug(false) once you have what you need.
pgBully reports who leads; acting on it is your responsibility. Common patterns:
Singleton scheduled job — only the leader runs it:
SELECT run_job() WHERE pgbully.is_leader();Connection routing — a proxy/health check that only reports a node healthy when it leads:
-- health endpoint returns 200 only on the leader
SELECT pgbully.is_leader();Choosing a write target — application picks the node where
pgbully.is_leader() is true.
SELECT pgbully.force_election();Set pgbully.enabled = off and reload — the node relinquishes leadership and
stops participating without being removed from pgbully.nodes:
ALTER SYSTEM SET pgbully.enabled = off;
SELECT pg_reload_conf();Re-enable with on + reload.
- Update
pgbully.nodeson every node to the new membership. SELECT pg_reload_conf();on every node.- On a new node, also set its
pgbully.node_id(restart, since it isPOSTMASTER-context) and runCREATE EXTENSION pgbully.
Because the membership list is static config, keep it under configuration management so all nodes stay in sync.
pgbully.add_node(id, address, port) and pgbully.remove_node(id) make the
same change take effect immediately, without a reload — useful when a control
plane needs the cluster to react before configuration management catches up.
They write to shared memory only: pgbully.nodes is still the source of
truth, and the next reload restores it. Treat them as a way to apply a change
early, not as a way to skip step 1.
| Event | Result |
|---|---|
| Leader process/host dies | followers time out after election_timeout; the highest remaining id becomes leader |
| Dead leader returns (higher id) | it re-runs an election on startup and reclaims leadership |
Network blip shorter than election_timeout |
no election; heartbeats resume |
| Follower dies | no election; it is just marked unreachable |
Expected detection time after a leader failure is roughly election_timeout
plus the time to probe higher-id peers.
pgBully is a timeout-based election, not a quorum consensus protocol. Be aware of the following before relying on it for correctness:
- Partitions can produce two leaders, temporarily. If the network splits,
each side may elect a leader within its partition. The monotonic term
resolves this automatically when the partition heals — the lower-term leader
observes a higher term and steps down — but during the split both sides may
believe they lead. If two simultaneous writers would corrupt your data, add
an external fence (e.g. a shared lock, a lease in a quorum store, or storage
fencing) and gate it on
pgbully.is_leader(). - Leadership is advisory. pgBully never blocks writes or moves data. A node
reporting
is_leader() = trueis only telling you the election result. - Clock independence. pgBully uses elapsed-time intervals, not wall-clock
comparisons between nodes, so it does not require synchronized clocks — but
very large scheduling stalls (e.g. a VM frozen longer than
election_timeout) can still trigger an election.
For the algorithmic details behind these points, see algorithm.md.
The extension is not in shared_preload_libraries, or the server was not
restarted after adding it. Fix the setting and restart.
- Check
pgbully.node_idis set,> 0, and present inpgbully.nodeson every node (the worker logs a warning and idles otherwise). - Verify peers can actually connect: from one node,
psql 'host=… port=… dbname=…'using the exact conninfo frompgbully.nodes. Checkpg_hba.confand credentials. - Confirm
CREATE EXTENSION pgbullyran in thedbnameeach conninfo targets; otherwiserpc_*calls fail and peers look unreachable.
election_timeout is too close to heartbeat_interval, or the network is
dropping heartbeats. Increase election_timeout (rule of thumb: ≥ 3–5×
heartbeat_interval) and ensure connect_timeout < heartbeat_interval.
Another node has an equal or lower id and cannot see the higher-id leader.
Check reachability with pgbully.peers() from each side; a one-way network
path (A can reach B but not vice versa) is a common culprit.
pgbully.test() checks the three things that must hold before the worker will
do anything — shared memory attached, pgbully.node_id set, and that id
present in pgbully.nodes — and warns about whichever one failed:
SELECT pgbully.test();Look for pgbully: election worker started in the log. If absent, the library
failed to preload — check shared_preload_libraries and the server log for
load errors. The worker auto-restarts 5 seconds after a crash.