Architecture

PostgreSQL Connection Pooling with PgBouncer: A Practical Guide

"Too many connections" errors, memory pressure and slow logins usually have the same cure: a connection pooler. How PgBouncer works, which pool mode to choose, how to size the pools and which traps to avoid.

· 10 min read · AG Data Team

Sooner or later almost every growing PostgreSQL installation meets the same error: FATAL: sorry, too many clients already. The instinctive fix is to raise max_connections. That usually makes things worse. The real fix is PostgreSQL connection pooling, and the most widely used tool for it is PgBouncer — a small, single-purpose proxy that lets thousands of application connections share a few dozen real database connections.

Why PostgreSQL needs a connection pooler

PostgreSQL uses a process-per-connection model: every client connection gets its own backend process on the server. That design is robust, but each process costs memory and CPU scheduling, and opening a new connection — authentication, process start, catalog caches — takes milliseconds, not microseconds. The default max_connections is 100 for a reason.

Modern applications make this worse. Every application instance, worker, cron job and serverless function keeps its own connection pool, so ten containers with a pool of 20 already need 200 connections — most of them idle most of the time. Idle connections are not free: they hold memory, and a large number of backends makes some internal operations, such as taking snapshots, more expensive for everyone.

A pooler breaks the link between "number of clients" and "number of database processes". Applications connect to PgBouncer as if it were PostgreSQL; PgBouncer hands them a real server connection only when they actually need one.

The three pool modes

The PgBouncer documentation describes three modes. Choosing the right one is the most important decision you will make:

For most web applications, transaction pooling is the right default — provided the application is compatible with it.

What breaks in transaction mode

Because consecutive transactions from the same client may run on different server connections, anything that relies on session state is unsafe. According to the PgBouncer feature table, these do not work in transaction mode:

Protocol-level prepared statements — the kind most drivers create automatically — were also a problem for years. Since PgBouncer 1.21, released in October 2023, they are supported in transaction mode when max_prepared_statements is set to a non-zero value. If you run an older PgBouncer, upgrading is often simpler than reconfiguring every driver.

A practical pattern: run two pools. Normal application traffic goes through a transaction-mode pool; the few components that genuinely need sessions — a LISTEN-based worker, a migration tool, long-running admin jobs — connect through a small session-mode pool or directly to PostgreSQL.

Sizing the pools

The key PgBouncer settings are few:

A database server can only run as many queries in parallel as it has CPU cores and I/O capacity. A pool of a few times the number of CPU cores is a reasonable starting point for OLTP workloads; beyond that, extra connections mostly add contention. Start small, measure, and grow the pool only while throughput keeps improving. Remember to keep max_connections on PostgreSQL above the total of all pools plus room for administrators, replication and monitoring.

Monitoring PgBouncer

PgBouncer has an admin console: connect to the virtual pgbouncer database and run SHOW POOLS; and SHOW STATS;. Watch in particular:

Long-running or idle-in-transaction sessions are the classic way to drain a transaction pool: each one pins a server connection. Setting idle_in_transaction_session_timeout in PostgreSQL protects the pool from forgotten transactions. If the queries themselves are slow, start with our PostgreSQL slow query tuning checklist.

Where to run PgBouncer

Managed services offer their own equivalents, such as Amazon RDS Proxy, and alternatives like Pgpool-II, Odyssey and PgCat exist. PgBouncer remains the default choice because it is small, fast, well understood and does one job well.

Key takeaways

PostgreSQL PgBouncer Connection Pooling Performance
Work with us

Hitting connection limits in PostgreSQL?

We design and tune connection pooling for PostgreSQL — from a single PgBouncer in front of one server to pooled, highly available clusters.

Get in touch More from the blog