PostgreSQL Connection Pooling with PgBouncer
Install PgBouncer, configure transaction-mode pooling, and stop exhausting PostgreSQL's connection limit under load. Includes config, auth, and troubleshooting.
PostgreSQL spawns a process for each connection. At a hundred concurrent connections that is manageable. At a few hundred it starts consuming serious RAM and CPU on context switching — and most of those connections are idle most of the time. PgBouncer sits in front of PostgreSQL and maintains a small pool of actual database connections, multiplexing many application connections across them.
How pooling modes work
PgBouncer has three modes. The one you pick determines when a server connection is returned to the pool:
| Mode | Releases server connection | Use when |
|---|---|---|
| Session | When the client disconnects | You need SET, LISTEN, prepared statements |
| Transaction | After each transaction commits | Stateless apps, most web backends |
| Statement | After each statement | Rare — autocommit only, no multi-statement transactions |
Transaction mode is the right choice for most web applications. It gives the highest connection reuse with acceptable constraints: no session-level state (SET, LISTEN/NOTIFY), and named prepared statements require extra config.
Install PgBouncer
sudo apt install pgbouncer # Debian / Ubuntu
sudo dnf install pgbouncer # RHEL / Fedora / Rocky
Configure PgBouncer
The main config file is /etc/pgbouncer/pgbouncer.ini. A working transaction-mode setup:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 5432
; Authentication
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
; Pooling
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
; Logging
logfile = /var/log/postgresql/pgbouncer.log
pidfile = /var/run/postgresql/pgbouncer.pid
Key settings:
max_client_conn— maximum total connections PgBouncer will accept from applicationsdefault_pool_size— maximum actual connections to PostgreSQL per database/user pair- Keep
default_pool_sizeunder PostgreSQL'smax_connections(default 100) with headroom for admin sessions
Set up the user list
PgBouncer authenticates clients against /etc/pgbouncer/userlist.txt. Get the password hash directly from PostgreSQL:
SELECT concat('md5', md5('secretpassword' || 'appuser'));
Write the result to userlist.txt (one entry per line):
"appuser" "md5a8b56ed42f7c98a1c1e7ccc0f6e1bde2"
The md5 value is md5 + md5 hash of password + username (concatenated, no separator).
Set permissions:
sudo chown postgres:postgres /etc/pgbouncer/userlist.txt
sudo chmod 640 /etc/pgbouncer/userlist.txt
Start and enable PgBouncer
sudo systemctl enable --now pgbouncer
sudo systemctl status pgbouncer
Connect through the pool
Your application connects to PgBouncer the same way it connects to PostgreSQL — just point it at PgBouncer's address and port. When PgBouncer is on a different port (common when PostgreSQL is also on the same host), use port 6432:
listen_port = 6432
postgresql://appuser:secretpassword@127.0.0.1:6432/mydb
Monitor the pool
Connect to PgBouncer's virtual admin console:
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
Useful commands inside the console:
SHOW POOLS; -- pool status, client count, server count per database/user pair
SHOW CLIENTS; -- connected clients
SHOW SERVERS; -- backend connections to PostgreSQL
SHOW STATS; -- request throughput and latency
SHOW CONFIG; -- current running config
SHOW POOLS output:
database | user | cl_active | cl_waiting | sv_active | sv_idle | maxwait
----------+---------+-----------+------------+-----------+---------+---------
mydb | appuser | 45 | 0 | 18 | 2 | 0
cl_waiting > 0 means clients are waiting for a connection from the pool — increase default_pool_size or check for long-running transactions holding connections open.
Reload config without downtime
sudo systemctl reload pgbouncer
Or from the admin console:
RELOAD;
Prepared statements in transaction mode
Named prepared statements are session-scoped in PostgreSQL. In transaction mode, the server connection is returned to the pool after each transaction, so the prepared statement is gone on the next transaction.
Options:
- Use unnamed prepared statements (extended query protocol,
$1placeholders) — most drivers handle this automatically. - Enable
server_reset_query = DEALLOCATE ALL— PgBouncer runs this before returning a connection, cleaning up named statements. - Switch to session mode if your app depends heavily on
PREPARE/EXECUTE.
Most ORMs and connection pool libraries work fine in transaction mode without any changes. Test with your actual driver before assuming a problem.
Per-database pool overrides
Different databases can have different pool sizes:
[databases]
hightraffic = host=127.0.0.1 port=5432 dbname=hightraffic pool_size=40
reporting = host=127.0.0.1 port=5432 dbname=reporting pool_mode=session pool_size=5
The reporting database uses session mode (for long-running analytical queries) while hightraffic stays on transaction mode with a larger pool.
Set the PostgreSQL connection limit to match
PgBouncer's pool is only useful if PostgreSQL can accept the pool connections. Check and raise PostgreSQL's limit in /etc/postgresql/*/main/postgresql.conf:
max_connections = 100
A practical formula: default_pool_size × number of databases × number of PgBouncer instances, plus 5 connections for admin/monitoring headroom. Restart PostgreSQL after changing max_connections:
sudo systemctl restart postgresql
For more on query performance after reducing contention, see How to Read a PostgreSQL EXPLAIN ANALYZE Output. The EXPLAIN Query Analyzer can parse and summarize query plans without manual interpretation.
SysEmperor