PgBouncer relays bytes. Pgpool-II reads them. It speaks the PostgreSQL frontend and backend protocol in both directions, parses every statement with an imported copy of PostgreSQL’s own SQL parser, and uses what it finds to decide which backend the statement should go to. That single design decision is the source of everything Pgpool-II can do that PgBouncer cannot - read/write splitting, automatic failover, query cache - and of every failure mode it adds.
Pgpool-II gives you four things beyond pooling:
| Capability | What it means in practice |
|---|---|
| Load balancing | SELECTs are distributed across backends; writes go to the primary |
| Automatic failover | A failed backend is detached and failover_command is run to promote a standby |
| Query routing | Statements are classified by the parser and sent to primary or standby accordingly |
| Watchdog | Multiple Pgpool-II instances coordinate, hold a shared virtual IP, and vote on failover |
The cost is that Pgpool-II is a multi-process server with its own clustering state, its own quorum logic, its own SQL parser, and its own version-coupled understanding of PostgreSQL grammar. If the parser mis-classifies a statement, the query goes to the wrong node. If the watchdog partitions, you can get two virtual IP holders. Neither failure exists in a PgBouncer deployment.
Many teams therefore run PgBouncer for pooling and Patroni for failover instead, with HAProxy or keepalived owning the VIP. That stack has more moving parts on paper but each part does one job, and Patroni’s failover decisions are grounded in a real consensus store (etcd, Consul, ZooKeeper) rather than in Pgpool-II’s own watchdog. Pgpool-II earns its place when you specifically want in-process read/write splitting without teaching the application about replicas, or want the query cache, or want one component instead of three.
Clustering mode is chosen once and never changed
backend_clustering_mode selects the model. The modes are mutually exclusive and cannot be changed
after the server starts, so this is an early design decision, not a tunable.
backend_clustering_mode |
Who synchronises the data | Load balancing | Online recovery |
|---|---|---|---|
streaming_replication |
PostgreSQL | Yes | Yes |
snapshot_isolation |
Pgpool-II, synchronously | Yes | Yes |
native_replication |
Pgpool-II, synchronously | Yes | Yes |
logical_replication |
PostgreSQL | Yes | No |
raw |
Nobody - your problem | No | No |
Use streaming_replication. It is the mode the documentation calls the most recommended way to use
Pgpool-II, and it is the only one where PostgreSQL remains the authority on replication. The trade-off
is stated plainly upstream: visibility consistency among nodes is not guaranteed, so a read routed
to a standby immediately after a write may not see it.
snapshot_isolation is the interesting outlier: Pgpool-II performs the writes itself against every
node and does guarantee visibility consistency across the cluster. The price is that the transaction
isolation level must be REPEATABLE READ (default_transaction_isolation = 'repeatable read' in
postgresql.conf) and that throughput is worse than streaming replication mode because of the
consistency bookkeeping.
logical_replication has a sharp edge: logical replication does not replicate every table, but
Pgpool-II load balances every table. A table you forgot to publish will be read stale from the
subscriber, silently.
Pooling and load balancing are separate switches
These are two independent booleans, and the names invite confusion.
| Parameter | Default | Effect | Changeable |
|---|---|---|---|
connection_cache |
on |
Cache and reuse backend connections | Restart |
load_balance_mode |
on |
Distribute SELECTs across backends | Restart |
You can run with balancing and no pooling, pooling and no balancing, both, or neither. They interact
only through sizing. Connections to the template0, template1, postgres and regression
databases are never cached even when connection_cache = on.
Pgpool-II’s pooling model is not PgBouncer’s. Each child process handles exactly one client at a time
and holds up to max_pool cached backend connections of its own; there is no shared pool. The
ceiling on backend connections is therefore num_init_children * max_pool, which with the defaults
of 32 and 4 is 128 connections per Pgpool-II instance. The documented constraint is:
max_pool * num_init_children <= (max_connections - superuser_reserved_connections)
and doubled if you need query cancellation to work, because cancelling a query opens another connection to the backend. Health checking opens one more connection per backend node on top of that.
num_init_children is also the client concurrency limit. Clients beyond it are blocked, not
rejected, up to listen_backlog_multiplier * num_init_children (default multiplier 2) queued in the
kernel listen queue. Set reserved_connections to 1 or more if you would rather they get “Sorry, too
many clients already” than block; the default is 0.
This is the honest weakness relative to PgBouncer. max_pool = 4 and num_init_children = 32 do not
give you 5000 clients over 25 backend connections. Pgpool-II’s pooling is a per-process cache, not a
multiplexer, and pooling is not the reason to deploy it.
What actually gets load balanced
The balancing node is chosen once at session start and does not change for the life of the session
unless statement_level_load_balance is on (default off). For long-lived application pools that
matters: without it, one connection means one standby, forever.
A query is eligible for balancing only if all of the following hold:
- The mode is streaming replication, native replication, snapshot isolation, or logical replication, never
raw. - It is not
SELECT INTO, notSELECT ... FOR UPDATE, notSELECT ... FOR SHARE. - It starts with
SELECT, or isCOPY TO STDOUT,EXPLAIN, orEXPLAIN ANALYZE SELECT. - It is not inside an explicit
BEGIN ... ENDblock (with exceptions, below). - In streaming replication mode additionally: no temporary tables, no unlogged tables, no system catalogues. None of the three are replicated, so reading them from a standby is wrong.
The explicit-transaction rule is softer than it looks. Inside a BEGIN block, load balancing is
still possible provided the isolation level is not SERIALIZABLE and no write query has been issued
yet. A write query here means a non-SELECT DML or DDL statement. Once one appears, subsequent reads
stop being balanced according to disable_load_balance_on_write.
That parameter is the real read-your-writes control:
disable_load_balance_on_write |
Behaviour after a write | Cost |
|---|---|---|
off |
Reads still balanced | May read stale data unless synchronous_commit = 'remote_apply' |
transaction (default) |
Reads not balanced until the transaction ends | Balanced default |
trans_transaction |
Not balanced for the rest of the session’s explicit transactions | Safer, slower |
always |
Not balanced for the rest of the session | Highest compatibility, lowest performance |
dml_adaptive |
Not balanced only for tables written in this transaction | Needs dml_adaptive_object_relationship_list for triggers and views |
Functions are the other thing that quietly disables balancing. If both write_function_list and
read_only_function_list are empty (the recommended configuration), Pgpool-II checks the function’s
volatility in the system catalogue and treats any VOLATILE function as a writing function, so a
SELECT calling it is not balanced. The lookup costs one extra catalogue query the first time per
function; the result is cached afterwards.
Two more routing controls worth knowing. backend_weightX sets the relative share of balanced
queries each node receives. database_redirect_preference_list overrides per database:
database_redirect_preference_list = 'postgres:primary,mydb[01]:1(0.3),mydb2:standby'
That sends all SELECTs on postgres to the primary, 30% of SELECTs on mydb0/mydb1 to node 1, and
all SELECTs on mydb2 to a randomly chosen standby. There are sibling parameters for user and
application name; the precedence is app_name_redirect_preference_list >
database_redirect_preference_list > user_redirect_preference_list.
Finally, replication lag. With delay_threshold (bytes) or delay_threshold_by_time
(milliseconds, and it wins if greater than 0) set above 0, Pgpool-II stops sending SELECTs to a
standby that has fallen behind and routes them to the primary instead, even with load_balance_mode
on. Both default to 0, which means lag checking is off and a badly lagged standby will keep
serving reads. Turn one of them on.
Health checks and failover
Health checking is disabled by default: health_check_period is 0. Without it, failover is
driven only by errors on live sessions, which means a backend that dies while no client is connected
is not noticed at all.
| Parameter | Default | Notes |
|---|---|---|
health_check_period |
0 (disabled) | Seconds between checks |
health_check_timeout |
20 | Seconds before giving up on the TCP connect |
health_check_max_retries |
0 (no retry) | Retries before declaring failure |
health_check_retry_delay |
0 | Seconds between retries |
health_check_user |
'' |
Must exist on every backend; empty is an error from 4.6 |
connect_timeout |
10000 ms | Also used for ordinary pool connections |
failover_on_backend_error |
- | Treat a read/write error as node failure |
failover_on_backend_shutdown |
off |
Fail over on SQLSTATE 57P01/57P02 (4.3+) |
search_primary_node_timeout |
300 s | 0 means retry forever |
A runnable streaming-replication fragment:
backend_clustering_mode = 'streaming_replication'
listen_addresses = '*'
port = 9999
pcp_listen_addresses = '*'
pcp_port = 9898
num_init_children = 64
max_pool = 4
connection_cache = on
reserved_connections = 1
backend_hostname0 = '10.0.1.10'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/var/lib/pgsql/17/data'
backend_flag0 = 'ALLOW_TO_FAILOVER'
backend_application_name0 = 'pg0'
backend_hostname1 = '10.0.1.11'
backend_port1 = 5432
backend_weight1 = 1
backend_data_directory1 = '/var/lib/pgsql/17/data'
backend_flag1 = 'ALLOW_TO_FAILOVER'
backend_application_name1 = 'pg1'
load_balance_mode = on
statement_level_load_balance = off
disable_load_balance_on_write = 'transaction'
sr_check_period = 10
sr_check_user = 'pgpool_check'
sr_check_database = 'postgres'
delay_threshold_by_time = 5000
prefer_lower_delay_standby = on
health_check_period = 10
health_check_timeout = 20
health_check_user = 'pgpool_check'
health_check_max_retries = 3
health_check_retry_delay = 2
failover_on_backend_error = off
failover_on_backend_shutdown = off
failover_command = '/etc/pgpool-II/failover.sh %d %h %p %D %m %H %M %P %r %R'
follow_primary_command = '/etc/pgpool-II/follow_primary.sh %d %h %p %D %m %H %M %P %r %R'
failover_on_backend_error and failover_on_backend_shutdown are both off here deliberately.
SQLSTATE 57P01 is emitted not only on shutdown but whenever a backend is killed by
pg_terminate_backend or SIGTERM, so leaving failover_on_backend_shutdown = on turns a routine
session kill into a cluster failover. With retries enabled, health checking is the more trustworthy
signal; the documentation also advises disabling failover_on_backend_error if you use
health_check_max_retries.
failover_command is a script you write. Pgpool-II substitutes these before running it:
| Token | Value |
|---|---|
%d |
Node ID of the detached node |
%h / %p / %D |
Hostname, port, data directory of the detached node |
%m / %H / %r / %R |
New main node’s ID, hostname, port, data directory |
%M |
Old main node ID |
%P |
Old primary node ID |
%N / %S |
Hostname and port of the old primary (4.1+) |
“Main node” is not “primary”. It means the live node with the smallest node ID, which in streaming
replication mode is frequently a standby. If every standby is down and the primary fails, the command
is invoked with %m = -1 and %H, %R, %r empty. Your script must handle that case, not promote
whatever %H happens to contain.
A detached node never comes back on its own by default. Reattach with pcp_attach_node, or set
auto_failback (4.1+). Confirm replication_state is streaming before you do. follow_primary_command
runs only for primary failover, in a forked child so it does not block service, and typically calls
pcp_recovery_node to rebuild each standby from the new primary.
Watchdog, the delegate IP, and split brain
One Pgpool-II is a single point of failure, so watchdog runs several and elects a leader. The leader
holds delegate_ip, the virtual IP clients actually connect to, brought up and down with if_up_cmd
and if_down_cmd (defaults use sudo ip addr add/del $_IP_$/24 dev eth0) and announced with a
gratuitous ARP via arping_cmd. delegate_ip defaults to empty, which means no VIP is ever brought
up.
| Parameter | Default | Notes |
|---|---|---|
use_watchdog |
off |
Restart to change |
wd_portX |
9000 | Watchdog inter-node port |
heartbeat_portX |
9694 | UDP heartbeat port |
wd_lifecheck_method |
heartbeat |
Or query, or external |
wd_interval |
10 s | Life check interval |
wd_authkey |
'' (disabled) |
Shared key for all watchdog traffic and IPC |
failover_when_quorum_exists |
on |
Require quorum before failing a backend over |
failover_require_consensus |
on |
Require a majority vote, not just quorum |
enable_consensus_with_half_votes |
off |
Allow exactly half the votes to count |
You need at least three Pgpool-II nodes to avoid split brain. Quorum means a majority of watchdog nodes are live: three of five, two of three. The VIP will not be brought up without quorum.
failover_require_consensus is the setting that makes Pgpool-II’s HA defensible. With it on, a
single node that mistakenly believes a backend is dead cannot fail it over; it must persuade a
majority. What happens instead is quarantine: that node stops using the backend locally, does not
propagate the state to its peers, and does not run failover_command. The consequence to internalise
is that a quarantined primary is not replaced: the standby is never promoted, and that Pgpool-II
node has no usable primary until the situation resolves. From 4.1 a leader that fails to build
consensus for a primary failover resigns and lowers its wd_priority so a different node is elected.
Quarantined nodes stay health-checked and reattach automatically once reachable (4.1+).
Because consensus requires votes, enable health checking on every Pgpool-II node. A node that cannot independently detect the failure cannot vote for it, and the failover stalls.
enable_consensus_with_half_votes only matters with an even number of nodes and the documentation is
blunt about the risk: in a four-node cluster split into two pairs, both pairs have “quorum”, both
elect a leader, and both raise the VIP. Leave it off. The related 4.3 dynamic-membership settings
(wd_remove_shutdown_nodes, wd_lost_node_removal_timeout, wd_no_show_node_removal_timeout, all
defaulting to off/0) shrink the cluster definition so quorum is computed against surviving members. That is useful, and it carries the same split-brain hazard. Defaults preserve pre-4.3 behaviour.
Set wd_authkey. Empty means watchdog authentication is disabled entirely, and watchdog packets
control your VIP.
Diagnostics
SHOW POOL_NODES is the first command to run and usually the last one you need. It is answered by
Pgpool-II itself over the Postgres protocol:
psql -h 10.0.1.100 -p 9999 -U app appdb -c 'SHOW POOL_NODES'
| Column | Meaning |
|---|---|
node_id |
Backend index, matching backend_hostnameX |
status |
What Pgpool-II believes: up, down, waiting |
pg_status |
What the backend actually reports, via PQpingParams (4.3+) |
lb_weight |
Normalised backend_weight, 0-1 |
role |
Pgpool-II’s view: primary or standby |
pg_role |
Real role from pg_is_in_recovery() (4.3+) |
select_cnt |
SELECTs sent to this node since startup; excludes internal queries |
load_balance_node |
true on the node this session is balancing to |
replication_delay |
Bytes, or seconds if delay_threshold_by_time > 0 |
replication_state / replication_sync_state |
From pg_stat_replication (4.1+) |
last_status_change |
Updated whenever status or role changes |
The status/pg_status and role/pg_role pairs exist precisely to expose disagreement between
Pgpool-II’s model and reality. status = down with pg_status = up is normal after
pcp_detach_node. role disagreeing with pg_role means Pgpool-II is routing writes to the wrong
node. Pgpool-II scans backends in node-ID order and assumes the last non-standby it finds is the
primary, so two primaries after a botched promotion produce exactly this. detach_false_primary
(default off, streaming replication mode, PostgreSQL 9.6+) uses pg_stat_wal_receiver to detach
the impostor automatically.
Empty replication_state and replication_sync_state usually mean configuration, not failure:
sr_check_period must be non-zero, backend_application_nameX must match the application_name in
that standby’s primary_conninfo, and sr_check_user must be a superuser or a member of
pg_monitor.
Administration is out-of-band over the PCP protocol on port 9898, authenticated from pcp.conf with
passwords optionally in ~/.pcppass (mode 0600 or it is ignored).
| Command | Use |
|---|---|
pcp_node_info -a |
Same data as SHOW POOL_NODES, scriptable |
pcp_watchdog_info --verbose |
Leader, members, and QUORUM EXIST / QUORUM ABSENT |
pcp_detach_node |
Planned switchover; bypasses quorum and consensus checks |
pcp_attach_node |
Reattach after repair |
pcp_recovery_node |
Rebuild a standby and attach it |
pcp_promote_node |
Promote a standby; triggers follow_primary_command |
pcp_health_check_stats |
Per-node health check counters, also via SHOW POOL_HEALTH_CHECK_STATS |
pcp_reload_config |
Reload without restart |
Note that PCP detach and attach deliberately bypass the quorum and consensus rules: they are operator intent, not detection. That is also why they are the escape hatch when quarantine has deadlocked a node.
What changed, and in which release
| Release | Date | Change that affects this page |
|---|---|---|
| 4.3.0 | 2021-12-07 | Shutting PostgreSQL down with pg_ctl no longer triggers an immediate failover, because new failover_on_backend_shutdown defaults to false; dynamic watchdog cluster membership (Member/Nonmember); pg_status/pg_role columns added to SHOW POOL_NODES; compile-time defaults corrected - load_balance_mode, failover_require_consensus, failover_when_quorum_exists and clear_memqcache_on_escalation to on, sr_check_period 0 to 10, listen_backlog_multiplier 32 to 2, authentication_timeout 0 to 60 |
| 4.4.0 | 2022-12 | delegate_IP renamed delegate_ip; socket_dir renamed unix_socket_directories; delay_threshold_by_time added; dynamic process management; backend ParameterStatus messages now forwarded to the frontend |
| 4.5.0 | 2023-12 | PREPARE/EXECUTE/DEALLOCATE can now be load balanced when the prepared statement is a read-only SELECT - previously always sent to the primary; user_redirect_preference_list added |
| 4.6.0 | 2025-02 | Defaults of health_check_user, recovery_user, sr_check_user and wd_lifecheck_user changed from 'nobody' to ''; unset now raises an error |
| 4.7.0 | 2025-12 | Slony mode removed; logdir renamed work_dir; recovery_database default changed from hardcoded template1 to postgres; log_pcp_processes default changed to off; watchdog and heartbeat receivers now bind only to configured addresses instead of all interfaces; protocol 3.2 and PostgreSQL 18 parser support |
Four of these break on upgrade rather than merely adding something. The 4.6 user defaults turn a
working 4.5 config into a startup error. The 4.7 recovery_database change breaks online recovery
for anyone whose extension lives in template1 - set recovery_database = 'template1' or create the
extension in postgres. The 4.7 watchdog bind change will break a cluster where hostname or
heartbeat_hostname was set to an address the node does not actually own. And 4.5’s load balancing
of prepared statements moves traffic that used to be guaranteed to hit the primary.
Where Pgpool-II is the wrong tool
If all you need is to stop PostgreSQL forking a backend per idle application connection, this is the
wrong tool. Pgpool-II’s pooling is a per-child-process cache bounded by num_init_children * max_pool,
not a multiplexer, and it does not deliver PgBouncer’s thousands-of-clients-over-tens-of-connections
ratio. Use PgBouncer.
If you need failover but the application already knows about replicas (separate read and write DSNs,
or a driver that handles target_session_attrs), then the query-routing half of Pgpool-II is dead
weight that can only misroute. Patroni plus HAProxy gives you failover grounded in a real consensus
store, without a SQL parser in the data path.
If your workload leans on session-scoped state, prepared statements built as plain SQL, or grammar newer than the PostgreSQL parser your Pgpool-II version imported, expect friction. Pgpool-II’s routing correctness depends on its parser understanding your SQL; 4.6 imported PostgreSQL 17’s parser and 4.7 imported 18’s, so running a newer PostgreSQL than your Pgpool-II was built against means statements it cannot parse fall back to being sent to the primary.
And Windows is not supported. Linux, FreeBSD and other UNIX-like systems only.
Pgpool-II release support
5 supportedFive stable branches are maintained concurrently, mirroring the PostgreSQL major versions they support. All five received a coordinated release on 2026-06-04.
| Release | Latest | Released | End of life | Status |
|---|---|---|---|---|
4.7 | 4.7.2 | - | not published | Supported |
4.6 | 4.6.7 | - | not published | Supported |
4.5 | 4.5.12 | - | not published | Supported |
4.4 | 4.4.17 | - | not published | Supported |
4.3 | 4.3.20 | - | not published | Supported |
Data as of 2026-10-01 from pgpool.net. Refresh with node scripts/fetch-versions.mjs.
PostgreSQL release support
5 supported| Release | Latest | Released | End of life | Status |
|---|---|---|---|---|
18 | 18.6 | Sept 2025 | Nov 2030 | Supported |
17 | 17.11 | Sept 2024 | Nov 2029 | Supported |
16 | 16.15 | Sept 2023 | Nov 2028 | Supported |
15 | 15.19 | Oct 2022 | Nov 2027 | Supported |
14 | 14.24 | Sept 2021 | Nov 2026 | Supported |
13 | 13.23 | Sept 2020 | Nov 2025 | End of life |
12 | 12.22 | Oct 2019 | Nov 2024 | End of life |
11 | 11.22 | Oct 2018 | Nov 2023 | End of life |
Data as of 2026-10-01 from endoflife.date. Refresh with node scripts/fetch-versions.mjs.
Where Pgpool-II sits
How this component relates to the rest of the stack. Items without a link are not covered here yet.