Postgres Connection Pooling in 2026: PgBouncer vs Supavisor vs Driver-Side Pools
Postgres forks a process per connection, so pooling is not optional past a few dozen clients. How PgBouncer, Supavisor, and driver-side pools differ, and what transaction mode breaks.
Postgres does not have a connection problem. It has a process problem. Every connection forks a dedicated backend process on the server, and that process keeps its own catalog cache, its own memory contexts, and its own slot in shared memory whether it is executing a query or sitting idle behind a keep-alive.
That design is fine when a monolith opens 40 connections and holds them for the life of the deploy. It stops being fine the moment your API runs on autoscaled containers, each with a driver pool of 20, or on serverless functions that open a connection per invocation. Twelve containers at 20 connections each is 240 backends against a max_connections of 200, and the failure mode is FATAL: sorry, too many clients already — arriving during the exact traffic spike that triggered the scale-out.
The three things people call “connection pooling”
These get conflated constantly, and they solve different halves of the problem.
Driver-side pooling is what HikariCP, pgxpool, node-postgres Pool, SQLAlchemy’s QueuePool, and Django’s CONN_MAX_AGE give you. It bounds and reuses connections within one process. Nothing coordinates across processes, so the cluster-wide total is whatever your replica count multiplies out to. This is usually what people mean when they say pooling is “built in” — it is built into the client library, not the server. Core PostgreSQL still ships no server-side pooler in a released version; proposals have circulated for years without landing.
An external pooler — PgBouncer, Supavisor, pgcat — sits between your app and Postgres as a proxy, accepting many client connections and multiplexing them onto a much smaller set of server connections.
A provider pooler is the same thing operated by someone else: RDS Proxy, Azure’s managed PgBouncer, Supabase’s hosted Supavisor, Neon’s proxy layer.
The important part: these compose rather than compete. The driver pool bounds concurrency per process and saves you the TCP plus TLS plus auth handshake on every query. The proxy bounds the total across the fleet. Removing the driver pool because you added PgBouncer just moves connection churn onto the pooler.
PgBouncer and Supavisor solve the same problem at different scales
PgBouncer is a small C daemon built around a single-threaded event loop. Footprint is tens of megabytes. It offers three pooling modes — session, transaction, and statement — and transaction mode is where the multiplexing payoff lives. Two changes in recent years matter: it can run several processes behind SO_REUSEPORT to use more than one core, and it can track protocol-level prepared statements in transaction mode via max_prepared_statements, which removed the single biggest reason ORMs used to break behind it.
What PgBouncer does not do: it is not cluster-aware, it has no read-replica routing, and configuration is a flat ini file you deploy and reload yourself. That is a feature if you want a boring, well-understood binary next to your database, and a limitation if the pooler itself becomes the tier you need to scale.
Supavisor is Supabase’s pooler, written in Elixir on the BEAM. Two design choices drive everything else. It is multi-tenant — one deployment fronts many databases, with the tenant identified from the connection string username — and it runs as a distributed cluster, so the pooler tier scales horizontally instead of vertically. It also does query load balancing across read replicas and can pause a tenant’s traffic for a failover or migration without dropping client sockets. The trade-off is a heavier runtime, and on Supabase you consume it as a hosted endpoint (transaction mode on port 6543, session mode and direct connections on 5432) rather than something you hand-tune.
pgcat is the third option worth knowing: Rust, sharding and load balancing built in, smaller community and less production mileage than either of the above.
| Pooler | Runtime | Scales by | Replica routing | Best fit |
|---|---|---|---|---|
| PgBouncer | C, single-threaded loop | Extra processes via SO_REUSEPORT | No | Self-hosted Postgres, one app, one DB |
| Supavisor | Elixir / BEAM cluster | Adding cluster nodes | Yes | Many tenants or many client sockets |
| pgcat | Rust | Threads | Yes, plus sharding | Teams that need sharding at the proxy |
| Driver pool only | In-process | Not at all across replicas | No | Fixed replica count, generous max_connections |
Transaction mode is the part that breaks your app
Transaction pooling hands the server connection back at COMMIT. Anything scoped to a session rather than a transaction either leaks into an unrelated request or disappears from under you.
The tedious half of that migration is finding every call site. Session-state dependencies hide in raw SQL strings, in ORM escape hatches, and in a SET statement_timeout someone added to one service three years ago. Grepping for SET , LISTEN, pg_advisory_lock, and CREATE TEMP gets you most of the way; reading each hit in context is the slow part.
Cursor
Codebase-wide search and edit with an AI agent — useful for the unglamorous part of a pooling migration: tracing every raw-SQL call site and session-scoped statement across services before you switch to transaction mode.
Free tier available; paid plans for larger context and faster models
Affiliate link · We earn a commission at no cost to you.
Measure the right thing before and after
Start with the question of whether you have a pooling problem at all. Run SELECT state, count(*) FROM pg_stat_activity GROUP BY state. If idle dwarfs active, connections are being held rather than used, and a pooler will help. If idle in transaction is your largest bucket, a pooler will not save you — that is application code holding a transaction open across a network call, and putting a proxy in front of it just moves the pileup one hop.
After you deploy the pooler, the two numbers that matter come from PgBouncer’s admin console. SHOW POOLS gives you cl_waiting and maxwait: a sustained non-zero maxwait means clients are queueing at the proxy, so you have relocated the queue rather than removed it — either the server pool is too small or your queries are too slow. SHOW STATS gives you avg_xact_time alongside avg_query_time; a wide gap between them is the signature of transactions held open around application work.
At the app layer, track p99 latency, not throughput. A pooler is a deliberate trade — a small queueing delay in exchange for not falling over at the connection ceiling — and throughput charts hide that cost while p99 shows it honestly.
FAQ
If I run PgBouncer, do I still need a pool in my driver?
Session mode or transaction mode?
Can I run Supavisor outside Supabase?
Related tools
Beehiiv
Newsletter platform with built-in ad network and Boost referrals.
Try Beehiiv →
Webflow
Visual site builder with real CSS export and a CMS that scales.
Try Webflow →
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-08-13
When There Is No API: Driving Chrome With the DevTools Protocol, and When Not To
CDP gives a scheduled agent a real browser when a site ships no API. Here is what it costs in memory and wall clock, the four failure modes that only surface on a cron, and the cases where you should not do it at all.
2026-08-12
Running a Small Kubernetes Cluster in 2026: k3s vs Talos vs a Managed Control Plane
k3s, Talos Linux, and a managed control plane solve different halves of the same problem. What each one actually costs you in money, upgrade work, and 2am debugging on a three-node cluster.
2026-06-22
Caddy vs Nginx in 2026: When Automatic HTTPS Is Worth the Switch
A practical comparison of Caddy and Nginx for solo developers and small teams: certificate management, performance trade-offs, config ergonomics, and when switching actually pays off.
2026-06-22
Hetzner vs OVH for Side Projects: Bare-Metal Value in 2026
A measured comparison of Hetzner and OVHcloud for side projects in 2026 — pricing models, bandwidth, hardware, and the trade-offs that actually matter for a solo developer.
2026-06-22
Bun vs Node.js in Production: What Actually Changes in 2026
A measured look at what swapping Node.js for Bun changes in real production: install speed, native TypeScript, built-in tooling, and the compatibility and observability gaps that still bite.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.