Postgres is solid, but its connection model is the part that tends to bite you. Each client connection maps to a dedicated backend process with real memory and CPU overhead, opening and closing connections is expensive, and even idle sessions hold resources. Picking a max_connections value is more guesswork than science: set it too low and your app queues, set it too high and the database burns RAM and scheduler time on connections that are mostly sitting there.
A connection pooler sits between your apps and Postgres and reuses a small set of server connections across many clients, which smooths out spikes and keeps the database from drowning.
Here's a quick, practical tour of the leading open-source options.
PgBouncer (the standard)
https://github.com/pgbouncer/pgbouncer
What it is. A lightweight, single-binary pooler that's been in production for years. It supports session, transaction, and statement pooling modes, each with its own trade-offs. Recent releases added useful touches like per-user/per-database connection tracking and limits and better TLS reload behavior, and prepared-statement support is now on by default.
Why it's popular:
- Small footprint and easy to deploy almost anywhere.
- Works with managed Postgres (RDS, AlloyDB, etc.) and on-prem clusters.
- Wired into many operators (CloudNativePG, for example, ships a
PoolerCRD).
Good fit for: most apps that need stable concurrency control and simple operations.
Cloudflare's fork of PgBouncer (cf-pgbouncer)
https://github.com/cloudflare/cf-pgbouncer
What it is. Cloudflare open-sourced their internal fork to harden multi-tenant operations. It adds auth bug fixes and features to enforce per-user and per-pool isolation, addressing cases where upstream behavior with HBA auth limited those controls. The fork is aimed at large-scale, multi-tenant environments where noisy-neighbor isolation actually matters.
Good fit for: high-scale providers and anyone who needs stricter tenant isolation and concurrency enforcement at the pooler layer. For more on the multi-tenant performance isolation goals behind it, see their companion post.
Supavisor (Supabase)
https://github.com/supabase/supavisor
What it is. A cloud-native pooler from Supabase, written in Elixir, that leans into horizontal scalability and multi-project tenancy. Supabase showed it handling roughly 1 million concurrent client connections in testing, which gives you a sense of an architecture built for massive fan-out: lots of lightweight client slots mapped onto a smaller set of server connections. It's now Supabase's default pooler.
Good fit for: SaaS platforms or services expecting very high connection counts (IoT, event streams, lots of short-lived clients) that want modern autoscaling patterns.
PgDog
https://github.com/pgdogdev/pgdog
What it is. A newer Rust project that rolls connection pooling, load balancing, and sharding into one layer. It supports session and transaction pooling "like PgBouncer," and advertises fan-out to hundreds of thousands of clients along with horizontal scale primitives. The docs include admin views (SHOW POOLS) and telemetry.
Good fit for: teams that want a single high-performance proxy that also handles routing and, eventually, scale-out patterns beyond plain pooling.
How to choose (and what to watch for)
1) Pick the pooling mode wisely
- Session pooling: one client to one server connection for the life of the session. Maximum compatibility (temp tables, GUCs, prepared statements all work) but the smallest concurrency gain.
- Transaction pooling: server connections go back to the pool after each transaction. This is the big concurrency win, but anything that relies on session state can break. Many ORMs are fine here as long as you avoid session-scoped behaviors.
- Statement pooling: the most aggressive mode, and rarely what you want outside of special cases.
2) Enforce limits at the pooler Use per-user and per-database caps to protect Postgres from thundering herds and to keep tenants in their lane. Upstream PgBouncer and Cloudflare's fork both give you controls here.
3) Mind prepared statements and features Prepared statements used to clash with transaction pooling. Newer PgBouncer releases improved the behavior and the defaults, but test your own ORM or framework rather than assuming.
4) Deploy for HA like any stateless proxy Run multiple poolers behind a VIP or load balancer, and set up health checks and drain/reload flows (this matters most with TLS). Cloud providers and operators like CloudNativePG can take some of this off your plate.
5) Tune the basics
Right-size server_pool_size, max_client_conn, and your timeouts against the workload's actual concurrency and transaction duration. Heroku's PgBouncer guidance is a clear mental model here (pools are per user/db/host tuple).
Quick comparison
| Project | Headline strengths | Maturity | Implementation language | Notes |
|---|---|---|---|---|
| PgBouncer | Lightweight, ubiquitous, three pooling modes | Very mature | C | Great default choice; wide community/packager support. |
| cf-pgbouncer | Multi-tenant isolation; auth fixes | Mature fork | C | Useful when you need stricter per-user/pool controls. |
| Supavisor | Cloud-native scale; demonstrated ~1M clients | Young → maturing | Elixir | Optimized for massive fan-out and provider use cases. |
| PgDog | Pooler + load balancer + sharder (Rust) | Emerging | Rust | Ambitious all-in-one proxy with horizontal scale features. |
Bottom line
- Start with PgBouncer for most apps. It's simple, well-documented, and proven in production.
- If you're a multi-tenant platform that needs hard per-user/pool isolation, evaluate Cloudflare's fork.
- If your problem is sheer connection volume (hundreds of thousands to millions), Supavisor is built for that world.
- If you also want routing and sharding alongside pooling, keep an eye on PgDog.
Whichever you land on, treat the pooler as part of your capacity and SLO strategy: cap concurrency, keep transactions short, and watch pool saturation so Postgres stays fast and happy.