PostgreSQL Load Balancing: 4 Options Explained

postgresql load balancing, postgres load balancing

Three different problems get called “load balancing” and they need different tools:

  • Connection pooling — too many connections for one server. PgBouncer, or Pgpool-II’s pooling.
  • Read load balancing — spreading SELECTs across replicas. Pgpool-II is the only one of these that reads the SQL and routes per statement.
  • Failover — promoting a replica when the primary dies. Patroni or pg_auto_failover. Pgpool-II can do it too, with a watchdog.

An earlier version of this page conflated all three, recommended Pgpool-II for everything, and named PgJDBC and Citus as load balancers. Neither is one, and the measurements below show what PgJDBC’s “load balancing” actually does.

Maintenance status, checked on 10 August 2026

All figures from the GitHub API on the date shown; the verify script re-fetches them.

project              last release            last push to the repo
Pgpool-II            4.7.2                   2026-08-07
PgBouncer            1.25.2  (2026-05-08)    2026-08-10
Patroni              4.1.4   (2026-07-07)    2026-08-10
HAProxy              3.4.0  (3.5-dev series) 2026-08-10
pg_auto_failover     2.2     (2025-04-03)    2026-08-11
Citus                14.2.0  (2026-08-06)    2026-08-10
pgJDBC               42.7.13 (2026-07-06)    2026-08-03
repmgr               5.5.0   (2024-11-22)    last commit on master 2025-04-17

repmgr is the one to think twice about. It is not archived and EDB has not announced its end, but there has been no commit on master for about sixteen months, and its own compatibility matrix — in the current source tree — lists repmgr 5.5 as supporting “13, 14, 15, 16, 17”. PostgreSQL 18 is not in that list. If you are building on 18, that is a concrete blocker, not a vibe. pg_auto_failover moved from the citusdata organisation to hapostgres; the old URL still redirects, and the repository is active even though 2.2 is over a year old.

PgJDBC is not a load balancer, measured

The old page credited PgJDBC with “a built-in load balancing mechanism” that “allows Java applications to distribute database workloads across multiple PostgreSQL servers”, plus connection pooling. Here is what it actually does, run on 10 August 2026 with pgjdbc 42.7.13 on OpenJDK 26.0.2, against a PostgreSQL 17.10 primary (55611) and its streaming standby (55612):

A. loadBalanceHosts=true, 40 connections x 3 queries each
  55611 primary          15      query->55611 primary   45
  55612 standby          25      query->55612 standby   75

B. loadBalanceHosts=false (the default), 40 connections
  55611 primary          40      query->55611 primary  120

Two things follow. It is off by default — B sends everything to the first host in the URL. And when it is on, the choice is made once per connection: 15 connections went to the primary and produced exactly 45 queries there, 25 went to the standby and produced 75. No statement is ever routed. It is random host selection at connect time, which is a failover convenience, not load balancing.

The consequence is not subtle. Thirty INSERTs over the same URL:

30 INSERTs over loadBalanceHosts=true: 11 succeeded, 19 failed
first failure: ERROR: cannot execute INSERT in a read-only transaction

Nineteen writes lost, because nothing in the driver knows an INSERT from a SELECT. The count is random: four repeats of the identical script lost 11, 15, 16 and 21 writes out of 30. An unpredictable failure rate is worse than a fixed one, not better. The fix is targetServerType, which does work — targetServerType=primary put 20 of 20 connections on the primary, and preferSecondary put 20 of 20 on the standby. Use those, and route reads and writes yourself in the application.

PgJDBC’s pooling is likewise not a thing you should use. PGPoolingDataSource still ships in 42.7.13, and its own Javadoc says: “@deprecated Since 42.0.0, instead of this class you should use a fully featured connection pool like HikariCP, vibur-dbcp, commons-dbcp, c3p0, etc.” The class comment adds that “the JDBC Driver is really not supposed to handle the connection pooling algorithm”.

Citus is not a load balancer and does not cost money

The old page listed Citus under load balancers and said “using Citus in a production environment involves licensing costs”. Citus shards tables across worker nodes; it changes where your data lives, not where a query is sent. And it is free software — the GitHub API reports its licence as AGPL-3.0, and the whole codebase has been open source since Citus 11. If you were avoiding it over a licence fee, that was based on a page that was wrong.

How to choose

If the problem is connection count, start with PgBouncer: it does one thing, it is small, and it is actively released. If you genuinely need SELECTs spread over replicas without changing the application, Pgpool-II is the tool that does it — see how its load balancing actually behaves, including the settings that silently disable it. If the problem is “what happens when the primary dies”, that is Patroni or pg_auto_failover, and HAProxy in front of them for the client-facing address; HAProxy alone balances TCP connections and has no idea which backend is writable.

What was not tested

Only PgJDBC was exercised here, plus the Pgpool-II measurements on the linked page. Patroni, repmgr, pg_auto_failover, PgBouncer and HAProxy were not deployed — this machine is macOS with no Linux host and no container runtime — so nothing above claims to have measured their behaviour. The dates and the repmgr compatibility matrix are read from their own repositories, and the tool-choice advice in the last section is judgement, not measurement.

Reproducing this

The script verify-load-balancing-ha-tools.sh re-fetches every maintenance date from the GitHub API, re-reads repmgr’s compatibility matrix and PgJDBC’s deprecation notice from source, and — if a JDK and network are available — rebuilds the primary/standby pair, downloads the driver, and re-runs all four connection experiments plus the failed-INSERT count. It reports 12 passed, 0 failed, 3 skipped on the machine above; if GitHub’s unauthenticated API budget is exhausted, section A skips rather than failing.

Check this yourself. Every command and every block of output on this page is reproduced by /verify/load-balancing-ha-tools.sh. Download it and run it: it creates its own scratch files, prints one line per claim, cleans up after itself, and exits non-zero if any claim here turns out to be wrong. If it disagrees with this page, the page is wrong.

Photo of author
Sudhir P. founded HeatWare.com in 1999 and has built and operated it full-stack ever since; it is now used by more than 88,000 people. He writes here about the PostgreSQL, MySQL, Linux and DevOps work that keeps it running. Articles are rewritten only after the commands in them have actually been run, and the verification scripts are published alongside them so anyone can check the claims. Reach him at blog@heatware.net.