Chapter 17: Sorting Out the Connection Poolers
For over a decade, the answer to "which connection pooler should I use?" was PgBouncer, because PgBouncer was the only serious answer. That is no longer the case.
The connection pooler landscape has expanded from a single incumbent into a competitive field of purpose-built tools, each making different architectural trade-offs. PgBouncer remains the default for most deployments, and I shall explain why. But its architectural limitations — a single-threaded design that saturates one CPU core — have created an opening for challengers that address workloads PgBouncer was never designed to handle.
This chapter evaluates four connection poolers: PgBouncer, PgCat, Supavisor, and Odyssey. Each receives an honest assessment based on architecture, benchmark performance, feature set, and maturity. I have included benchmark data from Tembo's 2024 comparison and Onidel's 2025 analysis, because opinions are plentiful in infrastructure discussions but measurements are rather more useful.
The evaluation criteria: throughput under load, latency at various concurrency levels, CPU efficiency, prepared statement support, replica failover capabilities, multi-threading, configuration complexity, and production maturity.
PgBouncer: The Incumbent
PgBouncer has been in production since 2007. In infrastructure years, this makes it approximately ancient — and in infrastructure, ancient is a compliment. Nineteen years of edge cases discovered, patched, and documented. Thousands of production deployments have validated its behaviour under conditions that no synthetic benchmark can replicate. When a database administrator recommends PgBouncer, they are recommending something they have personally relied upon, likely for years, without incident. That kind of trust is earned slowly and lost quickly.
Written in C with an asynchronous event-loop architecture, PgBouncer has the smallest resource footprint of any connection pooler. Configuration is a single INI file — pgbouncer.ini — that experienced administrators can set up in minutes. The ecosystem understands it intimately. The pgbouncer=true parameter in connection strings is a recognised convention across Prisma, Django, Rails, and every other framework in this book, signalling that prepared statements should be disabled for transaction mode compatibility.
In transaction mode — the mode Chapter 15 recommended for serverless workloads — PgBouncer assigns a database connection to a client only for the duration of a single transaction. The moment the transaction completes, the connection returns to the pool. For the serverless use case of "execute one query, return the result," this is ideal. The connection is borrowed for milliseconds and immediately available for the next client.
The single-threaded ceiling
PgBouncer's architecture is single-threaded and event-loop based. It cannot utilise more than one CPU core regardless of available hardware. On a 64-core server, PgBouncer uses one core.
Tembo's 2024 benchmarks quantify the ceiling precisely. PgBouncer peaks at approximately 44,000 transactions per second with 50 concurrent clients. Beyond 75 concurrent clients, throughput degrades — the single thread saturates, and additional clients queue rather than execute. CPU utilisation at peak throughput: approximately 100%, which on a single-threaded process means one core fully consumed.
The standard mitigation is running multiple PgBouncer instances behind a TCP load balancer — HAProxy is the common choice. Each instance runs on its own core, and the load balancer distributes connections. This works. It is also operationally more complex than a single process: multiple configurations to maintain, multiple processes to monitor, and a load balancer to manage. Teams that reach this point are often ready to evaluate PgCat instead.
Prepared statements in transaction mode
Transaction mode introduces a well-known constraint with prepared statements. A prepared statement is created on a specific PostgreSQL backend process. In transaction mode, the backend assigned to the next transaction may be different — the prepared statement does not exist there, and the query fails.
This is the source of the pgbouncer=true convention. When ORMs detect this parameter, they disable prepared statements, falling back to simple query protocol. The queries work, but they forfeit the performance benefit of prepared statement caching — parsing and planning happen on every execution rather than once.
PgBouncer version 1.21 introduced max_prepared_statements, which partially addresses this by caching PARSE messages and replaying them on new backend connections. This is a meaningful improvement, though the implementation is not yet equivalent to native session-mode prepared statement handling in all edge cases.
No native replica failover
PgBouncer connects to a single upstream PostgreSQL server. It has no concept of read replicas, no health checking of multiple backends, and no automatic failover between them. If you want to route reads to replicas, you need an external mechanism: HAProxy, DNS-based routing, Patroni, or a different pooler that supports it natively.
PgBouncer is the correct default for most deployments with straightforward architectures. If your concurrency regularly exceeds what a single CPU core can handle, if you need built-in replica failover, or if prepared statement performance matters in transaction mode — read on.
PgCat: The Challenger
PgCat is what happens when someone looks at PgBouncer's single-threaded architecture and asks what it would look like rebuilt from scratch with modern assumptions. Written in Rust, multi-threaded by design, and equipped with features that PgBouncer's architecture cannot support, PgCat targets the workloads where PgBouncer's ceiling becomes a floor.
Originally developed at PostgresML, PgCat is now community-maintained and has seen production deployment at organisations including Instacart. Configuration uses a TOML file (pgcat.toml), and PgCat maintains compatibility with PgBouncer's management commands — SHOW POOLS, RELOAD, and other administrative queries that operators have built monitoring around.
Benchmark performance
The Tembo 2024 benchmarks tell the comparison story with numbers rather than adjectives. PgCat reaches 59,000 transactions per second at 1,250 concurrent clients — 34% higher peak throughput than PgBouncer, achieved at 25 times the concurrency. The scaling curve is the critical difference: where PgBouncer peaks at 50 clients and degrades, PgCat continues climbing.
At low connection counts below 50, PgBouncer has marginally lower latency — the overhead of a simple event loop is slightly less than the overhead of thread coordination. At 50 connections and above, PgCat wins on both throughput and latency. The crossover point is remarkably precise.
CPU utilisation at peak throughput: 400% — four cores — at 1,250 clients. This is efficient multi-core utilisation rather than the single-core saturation that limits PgBouncer. On a machine with available cores, PgCat simply uses them.
Features beyond pooling
PgCat provides capabilities that PgBouncer does not attempt, because PgBouncer's architecture makes them difficult or impossible to implement:
Load balancing across read replicas is built in. No external HAProxy layer needed — PgCat distributes read queries across configured replicas natively. Read/write query splitting routes SELECT queries to replicas and writes to the primary, transparently.
Replica failover detection identifies failed replicas and routes around them automatically. If a replica goes offline, PgCat removes it from the rotation without operator intervention.
Sharding support enables query distribution across a sharded PostgreSQL deployment — a feature that bridges the connection pooler and the distributed query coordinator.
Prepared statements are supported in transaction mode. This is a meaningful advantage over PgBouncer's historical limitation, though the implementation details differ.
Maturity
The trade-off is age. PgCat was released in 2022 and reached its 1.0 milestone more recently. The production deployment base is growing but is materially smaller than PgBouncer's ecosystem, which has had 19 years to accumulate documentation, blog posts, Stack Overflow answers, and institutional knowledge. If you encounter an obscure issue with PgBouncer, someone has encountered it before. With PgCat, you may be the first.
PgCat is the right choice when your concurrency regularly exceeds 50 connections, when you need built-in replica failover, when you want read/write splitting without an external routing layer, or when prepared statement performance in transaction mode is important to your workload. For teams comfortable with a younger tool that offers genuine architectural advantages, PgCat represents the most compelling PgBouncer alternative available.
Supavisor: The Cloud-Native Option
Supavisor solves a different problem than PgBouncer or PgCat. Where those poolers optimise for single-tenant performance — one application, one database — Supavisor is designed for platform operators managing connection pools for thousands of tenant databases simultaneously.
Written in Elixir and purpose-built for Supabase's multi-tenant infrastructure, Supavisor is a cloud-native pooler that scales horizontally across a cluster of nodes. Its design goals are stated explicitly in its documentation: handle millions of connections across thousands of tenants, support zero-downtime database scaling (buffering and re-routing requests during compute resizing), and free up database instance resources by moving connection pooling to a dedicated adjacent cluster.
These are platform-level concerns. A team running one PostgreSQL database for one application does not need Supavisor's multi-tenant coordination. Supabase, running thousands of PostgreSQL instances for thousands of customers, does.
Benchmark context
Tembo's 2024 benchmarks show Supavisor peaking at approximately 21,700 transactions per second with 100 clients, with latency 80–160% higher than PgBouncer and PgCat at equivalent concurrency levels. CPU usage: 700% — seven cores — at 100 clients.
These numbers require context. Supavisor is doing substantially more work per connection than PgBouncer or PgCat. Multi-tenant routing, cluster coordination, distributed pool state management, and the Elixir/BEAM runtime overhead all contribute. The benchmarks measure single-instance performance, which is precisely the dimension Supavisor did not optimise for — its architecture is designed to scale horizontally by adding nodes, not vertically by extracting more throughput from one node.
Supavisor does not support protocol-level prepared statements — the kind used natively by JDBC, psycopg, and other driver-level implementations. For performance-sensitive applications that rely on prepared statement caching, Supabase's dedicated pooler (PgBouncer, available on paid plans) does support them.
The verdict
If you are a Supabase customer, Supavisor is your pooler. It is what Supabase provides, it handles the multi-tenant complexity transparently, and the integration is seamless: use the transaction mode connection string, set your pool size to 1 in serverless functions, and the platform manages the rest.
If you are self-hosting a single PostgreSQL deployment and need raw pooling performance, PgBouncer or PgCat are the stronger choices. Supavisor's strengths — multi-tenant management, zero-downtime scaling, horizontal cluster scaling — are platform capabilities that a single-tenant deployment does not need.
Odyssey: The Enterprise Option
Odyssey occupies a niche that the other poolers do not contest. Developed by Yandex for their internal PostgreSQL workloads — which operate at a scale few organisations match — it is a multi-threaded C pooler with enterprise-grade security features that the other options either lack or implement less comprehensively.
The standout capability is mutual TLS authentication. Where PgBouncer and PgCat support standard TLS termination, Odyssey supports mTLS — requiring both the client and the server to present valid certificates. For enterprise environments where regulatory compliance mandates mutual authentication, this is not optional.
Odyssey's prepared statement handling deserves specific mention. Both Odyssey and recent versions of PgBouncer implement an optimisation where identical prepared statements from multiple clients are mapped to a single internal identifier. If a thousand sessions prepare the same SELECT query, the pooler recognises the duplication and maps them to one prepared statement on the backend. This reduces PostgreSQL's overhead significantly for applications with repetitive query patterns — dashboards, reporting tools, and API endpoints that execute the same parameterised queries thousands of times per minute.
Benchmark performance from Onidel's 2025 analysis shows Odyssey consistently strong at high concurrency, benefiting from its multi-threaded architecture. CPU usage is higher than PgBouncer's single-thread efficiency but well-managed across available cores.
Odyssey is the right choice for enterprise environments requiring mutual TLS authentication, sophisticated prepared statement optimisation, or proven performance at Yandex-scale concurrency. For most other deployments, PgBouncer or PgCat offer simpler configuration and equally capable pooling.
What Poolers Do Not Do
The four tools in this chapter are connection poolers. They manage the relationship between client connections and database connections. They do this well — efficiently, reliably, and at scale. But it is worth stating clearly what they do not do, because the distinction matters for how you think about your performance architecture.
Connection poolers do not inspect the queries passing through them. They do not notice that a dashboard query is aggregating millions of rows when a materialized view could serve the result in milliseconds. They do not identify missing indexes. They do not detect N+1 query patterns. They do not cache results. They do not schedule materialized view refreshes.
A connection pooler ensures that your application can reach the database. It does not ensure that what the application does once connected is efficient. A pool of 20 well-managed connections executing poorly optimized queries is still a slow application — it is merely a slow application that no longer crashes with "too many connections."
This is why the optimization order from the preceding chapters matters. Connection pooling is step 1 — it keeps the doors open. Query optimization, indexing, and materialized views are steps 2 through 4 — they ensure that what passes through those doors is worthy of the connection it occupies. The pooler and the optimizations are complementary. Neither substitutes for the other.
Choosing Your Pooler
The decision, despite the length of this chapter, reduces to a few clear criteria.
Straightforward deployment, fewer than 50 concurrent connections: PgBouncer. The default recommendation. Nineteen years of production validation. The safest choice available, and the one I recommend when teams have no specific reason to choose otherwise.
High concurrency, 50 to 1,000+ connections, modern cloud-native architecture: PgCat. Multi-threaded scaling, built-in replica failover, read/write splitting, and prepared statement support in transaction mode. The architectural successor to PgBouncer for workloads that have outgrown a single thread.
Supabase customer: Supavisor. It is what Supabase provides, it handles the multi-tenant platform complexity, and the integration requires no additional configuration beyond using the correct connection string.
Enterprise environment requiring mutual TLS: Odyssey. The security features justify the additional configuration and operational complexity.
AWS Lambda to RDS or Aurora: RDS Proxy, unless you are using Prisma — in which case, see Chapter 15 for the connection pinning problem.
Edge functions on Cloudflare Workers or Vercel Edge: Neon's serverless driver or Cloudflare Hyperdrive. See Chapter 15.
Regardless of which pooler you choose: use transaction mode for serverless workloads, set your ORM's pool size to 1 in ephemeral functions, and monitor connection utilisation. The pooler manages the connections. You manage the configuration that determines how well it works.
The infrastructure is now in order. In Chapter 18, we bring everything together: a decision framework for PostgreSQL performance that tells you which optimisation to apply, in which order, for which symptom.