This article compares the architecture of the three poolers: language, pooling modes, single or multi-tenant model, functions beyond pure pooling. Tembo and PkgPulse have published numerical comparisons of these three tools, but we have not reproduced any of their measurements ourselves. Our editorial position on benchmarks, detailed in our benchmark methodology, is to never republish a figure that we have not verified ourselves. What you'll find here instead: the actual architecture of each tool, and how Aurabase actually routes its Postgres traffic, verified section by section in the source code.
- PgBouncer (C) remains the most proven pooler and best integrated with Kubernetes: CloudNativePG relies directly on it for its
Poolerresource. - Supavisor (Elixir, Supabase project) targets a different problem: serving thousands of databases from the same service, rather than one pooler per database.
- PgCat (Rust) adds application sharding, load balancing between replicas and automatic failover to raw pooling.
- The Aurabase repository shows PgBouncer used at two levels: a shared deployment for the shared fleet, and a
Poolerresource managed by CloudNativePG per dedicated tenant. Both operate in transaction mode. - PostgREST and the
aura-dbadministration pool voluntarily remain in direct connection to Postgres, without going through the pooler: transaction pooling would break their schema reloading and their session locks.
Three poolers, three philosophies
PgBouncer minimizes, Supavisor pools on a multi-tenant scale, PgCat adds network functions to raw pooling. None of the three is a direct replacement for the other two, although they are often compared term for term on the same pages.
| Language | C | Elixir (BEAM) | Rust |
|---|---|---|---|
| Pooling modes | Session, transaction, statement | Session, transaction | Session, transaction, statement |
| Tenancy model | One target cluster per instance, designed to be single-tenant | Native multi-tenant: a service for many databases | A target cluster, sharding by partition key |
| Beyond pooling | No additional functions, deliberately minimal | Admin HTTP API, dynamic tenant registration | Sharding, load balancing and failover between replicas |
| Native Kubernetes integration | Yes: CloudNativePG Resource Pooler | Not natively documented to date | Not natively documented to date |
| Origin | The historical standard of Postgres pooling | Built by Supabase for its own multi-tenant cloud | Born at Instacart, maintained today by PostgresML |
Columns, in order: PgBouncer, Supavisor, PgCat. Architecture characteristics according to the official filings of each project, to be confirmed on the version you are deploying, the ecosystem evolving quickly on this point.
The historic standard, lightweight and integrated into Kubernetes
PgBouncer only does one thing: pool Postgres connections, without any additional functions. This deliberately narrow scope largely explains its longevity and its adoption as a basic building block in most Postgres stacks in production.
Three pooling modes are available. Session mode opens one server connection per client connection, the most permissive. Transaction mode reuses a server connection between several clients, released at each end of a transaction. Statement mode goes even further, and is rarely used in production. It is the transaction mode that brings the real pooling gain, but it imposes strict rules. Any session state (SETvariables, advisory locks, LISTEN/NOTIFY) does not survive beyond a transaction. We detail these rules and their pitfalls in our dedicated article on the transaction pooling mode of PgBouncer.
Historically single-process, a PgBouncer instance uses a single CPU core by default. Running multiple instances behind the same port (via SO_REUSEPORT) is a more recent evolution of the project, not an initial design feature. On the authentication side, PgBouncer supports a configurable auth_query, an SQL function executed at each connection to dynamically resolve a role's password. This mechanism avoids depending on a static file listing each user in advance. It is exactly this mechanism that Aurabase uses for its per-project roles (section 05).
PgBouncer is the pooler that CloudNativePG natively deploys behind its Poolerresource. On a Postgres cluster managed by the CloudNativePG operator, activating a managed pooler amounts, in practice, to activating PgBouncer without configuring it by hand.
Supabase's cloud-native multi-tenant pooler
Supavisor addresses a problem that PgBouncer was never designed to solve at this scale. This involves serving a very large number of distinct tenant databases from a single service, rather than one pooler instance per database. Written in Elixir and executed on the Erlang virtual machine (BEAM), the project is developed and maintained by Supabase, in open source, on its own GitHub repository.
The native multi-tenant model is the real structural difference. Where a classic PgBouncer fleet requires one process (or a set of dedicated connections) per target base, Supavisor works differently. It dynamically registers tenants via an HTTP administration interface, and routes each incoming connection to the correct database without restarting the service. Supabase migrated its own Cloud projects from PgBouncer to Supavisor for this very reason. A classic pooling cluster, one per database, does not scale to a multi-tenant cloud that hosts hundreds of thousands of projects.
This architectural choice has a documented downside. Feature parity with PgBouncer on advanced cases took time to stabilize after project launch. Two examples: certain behaviors of LISTEN/NOTIFY, and the fine management of prepared statements in transaction mode. Check your version before migration if your application depends on these specific behaviors.
The Rust outsider: native sharding and load-balancing
PgCat is explicitly positioned as an alternative to PgBouncer, written in Rust. It adds network functions to classic pooling that neither PgBouncer nor Supavisor natively embeds. Three in particular: application sharding by partition key, load balancing between read replicas, and automatic failover away from a failed replica. The project was born at Instacart before being taken over and maintained today by PostgresML.
Concretely, PgCat can play the role that two distinct layers would normally occupy: a connection pooler and an application proxy for routing between several Postgres instances. A team that was already sharding its data by hand can simplify its code with PgCat. Same thing for a logic for distributing reads between replicas developed internally: a dedicated network layer directly replaces it.
The opposite compromise also exists: PgCat is a younger project, with a much smaller ecosystem of documentation and production feedback than PgBouncer. Adopting its sharding and failover functions also means agreeing to depend on the maturity of this specific component, not just on its pooling capacity.
What the Aurabase code shows: PgBouncer everywhere, except where transaction pooling breaks everything
The Aurabase repository deploys PgBouncer at two separate tiers, both in transaction mode. For the shared fleet, the Helm chart defines a dedicated PgBouncer deployment in front of the shared data-plane (deploy/helm/aurabase/templates/infra/pgbouncer.yaml, image edoburu/pgbouncer). For a tenant on a dedicated instance, the provisioner generates a resource Pooler natively managed by CloudNativePG (deploy/cnpg/tenant-pooler.yaml, rendered by k8s_tenant.rs). Neither uses Supavisor or PgCat. The code does not document an explicit comparison that preceded this choice. On the other hand, it shows a deep and already operational integration with the CloudNativePG ecosystem, consistent with the fact that PgBouncer is the native pooling brick.
However, not everything goes through the pooler, and this is a deliberate choice documented in the code itself. PostgREST remains connected live to Postgres, never via PgBouncer. The Helm chart comment is explicit about the reason: transaction pooling would break its schema reload, which depends on a LISTEN on the pgrstchannel. This mechanism is incompatible with recycled server connections between clients. Theaura-db administration pool (schema, DDL, session advisory locks) also remains in direct connection, for the same basic reason. Non-transaction-scoped SET search_path and session locks do not survive a transaction mode pooler.
Authentication follows the auth_query pattern described in section 02: AUTH_QUERY: SELECT * FROM pgbouncer.user_lookup($1), without a static userlist.txt file. This is what allows roles dynamically created per project (project_<uuid>_authenticator) to authenticate via PgBouncer without redeploying the pooler for each new project.
The PgBouncer healthcheck comment in the local Kubernetes manifests documents a real bug, already fixed. An pg_isready run against PgBouncer only validates the proxy handshake, never the actual connection to the Postgres backend it relays. PgBouncer responds “accepting connections” even when the backend is stopped, queuing the requests. Result observed during a destructive test: the service remained healthy for 5 consecutive cycles while Postgres was unreachable. The fix replaces the check with a true end-to-end psql request through the pooler, all the way to the backend. Result after correction, in the same test replayed: unhealthy detected in 7 cycles, approximately 35 seconds.
One last detail, minor but revealing: the Helm chart pins edoburu/pgbouncer:v1.24.1-p1 by default, while the local k3d bench uses v1.25.2-p0. It's not an architectural choice, just a slight lack of version synchronization between two environments, the kind of detail that a code review catches faster than a blog post. We document it as it is rather than dressing it up. For details of the schema partitioning that this pooler serves, see our article onmulti-tenant RLS isolation.
How to choose between the three
Choose PgBouncer if…
- Postgres cluster managed by CloudNativePG or Kubernetes in general
- You want the most proven and well-documented pooler
- A target base per pooler instance suits you
Choose Supavisor if…
- Hundreds or thousands of bases behind the same service
- Need to register tenants dynamically via an API, without redeployment
- Already in the Supabase ecosystem or willing to depend on it
Choose PgCat if…
- Application sharing already in place or planned at the pooler level
- Load balancing and failover replica without separate application layer
- Comfortable with a younger project, less documented than PgBouncer
Whatever pooler is chosen, it does not replace the sizing of Postgres itself. Pool size and server max_connections should be thought of together, not one after the other. A generous pool in front of a too low max_connections simply shifts the saturation from one level to another. Our guide on tuning max_connections details the sizing formula to apply before setting the size of your pool.
What we get asked most often
There is no universal pooler, only a good fit for your tenancy
PgBouncer, Supavisor and PgCat solve three variations of the same problem, not three versions of the same tool. PgBouncer remains the safest choice when your platform already relies on Kubernetes and CloudNativePG, or when you simply want the most documented pooler. Supavisor becomes relevant beyond a certain number of bases serving from the same service. PgCat is worth the detour if you miss sharding and replica failover at the network level, provided you accept the maturity of a younger project.
The Aurabase code shows a consistent, not neutral, choice: PgBouncer in transaction mode, at two levels, shared fleet and CNPG pooler per dedicated tenant. Two documented exceptions remain, for PostgREST and for schema administration. If you want to see this project siloing in action rather than on paper, our Performance page documents the associated measurement methodology.