PgBouncer is a single-purpose proxy that sits between your application and Postgres, accepting connections from potentially thousands of clients and funneling their queries through a small, fixed set of real connections to the database. It solves a specific problem: Postgres forks a full OS process per connection, and each one costs real memory even when idle, so a database server can only sustain a few hundred to a few thousand connections before it runs out of room. PgBouncer's own connection handling is extremely lightweight by comparison — a single-threaded event loop written in C — so it can hold far more client-side connections open than Postgres itself ever sees. The result is that your application can open connections liberally while Postgres only ever sees a small, predictable number.
What changed in 2026
- Supavisor and pgcat gained real production adoption as Rust-based alternatives built for multi-tenant and cloud-native setups, competing directly with PgBouncer's long-standing default status.
- Managed poolers became the norm for serverless. RDS Proxy, Neon's built-in pooler, and Supabase's Supavisor now ship pooling as a managed layer, reducing how often teams run PgBouncer themselves.
- PgBouncer added TLS and auth improvements in its 1.2x line, closing gaps that used to push security-conscious teams toward alternatives.
- Prepared statement support under transaction mode improved via newer client-side and server-side workarounds, narrowing one of PgBouncer's oldest limitations.
How PgBouncer actually pools a connection
A client connects to PgBouncer exactly as if it were Postgres — same protocol, same port convention. PgBouncer authenticates the client, then borrows one connection from its internal pool of already-open, already-authenticated Postgres connections and forwards traffic to it. When and how quickly that borrowed connection gets returned to the pool for reuse is controlled entirely by pool_mode.
;; pgbouncer.ini (essentials)
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 20
Here, up to 2,000 clients can connect to PgBouncer, but only 20 real connections per database-and-user pair are ever opened against Postgres itself.
Pool modes compared
| Pool mode |
Connection returned to pool |
Session state preserved |
Best for |
| Session |
After the client disconnects |
Yes, fully |
Apps relying on session state, lowest connection reuse |
| Transaction |
After each transaction commits or rolls back |
No, across transactions |
High-concurrency web APIs, the common default |
| Statement |
After each individual statement |
No, not even mid-transaction |
Extreme connection reuse; breaks multi-statement transactions |
Transaction mode is the right default for most web workloads: a request typically runs one short transaction, and returning the connection immediately afterward lets far more clients share the same small pool. The cost is that anything scoped to a session — SET search_path, session-level advisory locks, most prepared statement caching — cannot be relied on, because the next statement might land on a physically different Postgres connection.
Common mistakes
Using session mode by default "to be safe" and never revisiting it. Session mode holds a real Postgres connection for the entire life of the client connection, which defeats most of the point of pooling under real concurrency. Start with transaction mode and only fall back to session mode for the specific code path that needs it.
Sizing default_pool_size without checking Postgres's max_connections. PgBouncer can happily accept far more client connections than the pools behind it can serve; if multiple databases and users each get their own pool, the sum across all of them can still exceed what Postgres allows.
Relying on session-level SET statements in transaction mode. A SET application_name or SET statement_timeout issued outside a transaction may apply to a connection that gets handed to a different client next. Wrap session-scoped settings inside the transaction they belong to, or use SET LOCAL.
Running PgBouncer as a single instance with no redundancy. PgBouncer itself becomes a single point of failure between every client and the database; run it alongside your application instances or behind a supervised, restart-safe process, not as a lone box.
FAQ
Is PgBouncer a replacement for an application-level connection pool?
No, they solve different layers. An app-level pool manages connections within one process; PgBouncer sits in front of many app instances or serverless functions and reduces how many real connections all of them together open against Postgres.
Why do prepared statements break under transaction mode?
A prepared statement is tied to one physical Postgres connection. In transaction mode, the next statement from the same client can land on a different connection that never saw the PREPARE. Some drivers work around this automatically; others require disabling prepared statement caching.
How is PgBouncer different from RDS Proxy or Supavisor?
They serve the same purpose — connection multiplexing — but RDS Proxy and Supavisor are managed services with built-in high availability and IAM integration, while PgBouncer is software you run and operate yourself, giving more control at the cost of more ops work.
Does PgBouncer help with read replicas?
PgBouncer itself does not route reads versus writes; that decision still belongs to your application or a separate router. PgBouncer only reduces the connection count against whichever endpoint, primary or replica, it is configured to point at.
Where to go next
See read replicas explained for 2026 and database sharding explained for 2026 for the two scaling strategies PgBouncer commonly sits in front of, and use how to pick a database in 2026 if you are still deciding on the underlying database itself.