Skip to main content
Logo
Overview

PostgreSQL Connection Pooling in 2026: PgBouncer vs PgCat vs Supavisor vs RDS Proxy vs Pgpool-II

June 12, 2026
9 min read

If you landed here because your logs are full of FATAL: sorry, too many clients already, you already know the problem. The fix is a connection pooler. The hard part is picking one, and then not shooting yourself in the foot with the pooling mode you choose.

This trips up more teams than it should. Postgres handles connections in a way that punishes the exact access pattern modern apps have — lots of short-lived clients, especially from serverless functions. A pooler sits in the middle and reuses a small set of real database connections across a much larger crowd of app connections. Simple idea, but the five popular options behave differently enough that the wrong choice will cost you either throughput, correctness, or a weekend.

Why Postgres connections are so expensive

Postgres forks a full OS process for every connection. Not a thread — a process, with its own memory, roughly 5-10 MB of backend overhead before you’ve run a single query. A few hundred idle connections can eat gigabytes of RAM and chew CPU on context switches even when nobody’s doing anything.

Most managed Postgres caps you well below where you’d expect. A db.t3.medium on RDS tops out around 340 connections. A small Supabase or Neon instance, lower. Meanwhile your Lambda fleet scales to 500 concurrent executions, each one opening its own connection, and now you’ve blown the cap during a traffic spike. That’s the too many clients error. The connections aren’t even busy — they’re just there, holding a slot.

A pooler breaks the 1:1 link between “app wants a connection” and “Postgres spawns a backend.” Ten thousand clients can share fifty real backends if the workload allows it. The catch is in those last three words.

Pooling modes, and the gotchas that break apps

There are three modes, and this is the single most important thing to understand before you touch any of these tools.

Session pooling assigns a real backend to a client for the entire duration of its connection. It’s transparent — everything works exactly like a direct connection — but it barely helps, because an idle client still squats on a backend. Use it only when you genuinely need session-level features and can’t avoid them.

Transaction pooling is the one you almost always want. A backend is handed out per transaction and returned to the pool the instant the transaction commits. This is where the real multiplexing happens — a handful of backends serve a flood of clients. It’s also where things break if you’re not paying attention.

Statement pooling returns the backend after every single statement. It forbids multi-statement transactions entirely. Niche; you’ll know if you need it.

Here’s the part people learn the hard way. In transaction mode, anything that relies on session state across transactions is unsafe, because your next transaction might land on a different backend:

  • Session-level SET (like SET search_path or timezone) leaks or vanishes unpredictably. Use SET LOCAL inside the transaction instead.
  • Advisory locks taken with the session-scoped variants don’t stick — a different backend won’t see them.
  • LISTEN/NOTIFY doesn’t work, since the listening backend gets recycled.
  • Temporary tables scoped to the session disappear between transactions.
  • Prepared statements were the classic killer, and that one finally got fixed — more below.

None of this is the pooler’s fault. It’s the unavoidable cost of multiplexing. The tools differ in how gracefully they handle it.

The prepared-statement story finally has a happy ending

For years, the advice was “turn off prepared statements when you use PgBouncer in transaction mode,” which meant ORMs like Prisma needed pgbouncer=true flags and you ate a performance hit. That changed with PgBouncer 1.21, which added prepared-statement support in transaction mode. You set max_prepared_statements to a non-zero value (100 is a reasonable starting point), and PgBouncer transparently prepares the statement on whichever backend it routes you to.

This isn’t a minor footnote. The pganalyze and Crunchy Data writeups put the throughput gain anywhere from 15% to 250% depending on workload, because you stop re-parsing and re-planning the same queries on every execution. If you’re still running an older PgBouncer with prepared statements disabled, upgrading is close to free money.

PgCat and Supavisor support prepared statements in transaction mode too, so this particular landmine is mostly defused across the board in 2026. Just don’t assume — check the version you’re actually running.

The five contenders

PgBouncer is the boring, correct default. Single C binary, ultralight — on the order of 2 MB of RAM per thousand clients — and it’s been in production at basically every serious Postgres shop for over a decade. It does transaction pooling extremely well and now handles prepared statements. What it doesn’t do: read/write splitting, sharding, or load balancing. It’s a pooler, full stop, and that focus is exactly why it’s reliable.

PgCat is the Rust rewrite that asks “what if the pooler did more?” It speaks the Postgres protocol like PgBouncer but adds query routing to read replicas, sharding across multiple databases, load balancing, and failover, all configured as code. If you’ve outgrown PgBouncer’s single-target model and you’re hand-rolling read/write splitting in your app, PgCat moves that logic down into the infrastructure where it belongs. Younger and with a smaller operational track record, but maturing fast.

Supavisor is Supabase’s Elixir-based pooler, built for a different problem: enormous connection counts and multi-tenancy. It’s designed to handle hundreds of thousands of client connections across many tenant databases, which is precisely what a serverless or edge platform needs. The BEAM/Elixir runtime gives it cloud-native horizontal scaling that the single-process tools don’t have natively. The trade-off shows up in latency (more on that next).

RDS Proxy is the managed answer if you live on AWS. No servers to run, IAM authentication, automatic failover, and tight integration with RDS and Aurora. You give up control and pay per vCPU-hour, but you stop owning yet another piece of stateful infrastructure. For a lot of teams already deep in AWS, that’s the right trade.

Pgpool-II is the Swiss Army knife: connection pooling plus load balancing, read/write splitting, in-memory query caching, and replication management in one package. That breadth is also its weakness — it’s more complex to configure and operate, and the extra features mean more moving parts that can misbehave. Powerful when you need the whole toolkit, overkill when you just want pooling.

Benchmarks, read honestly

The Tembo team published the most-cited head-to-head, and the shape of the results matters more than any single number.

At low client counts (under ~50), PgBouncer has the best latency and throughput. It’s lean and there’s nothing in the way. As you push past 50-75 concurrent clients, the picture flips: PgBouncer peaks around 44,000 tps and then degrades into the 25,000-30,000 range under sustained load, while PgCat keeps climbing to roughly 59,000 tps with lower latency at high concurrency. PgCat’s Rust threading model simply handles the crowd better.

Supavisor is the slowest of the three on raw latency — the benchmarks show it running 80-160% higher latency than PgBouncer and PgCat. But that’s not the metric Supavisor is optimizing for. It’s built to keep hundreds of thousands of connections alive and routed across tenants, and to scale horizontally when one node isn’t enough. Judging it purely on single-node tps is like judging a freight train on its 0-60.

So read benchmarks for the workload they were run under. “PgCat is faster” is true at high concurrency on one node. “PgBouncer is faster” is true at low concurrency. “Supavisor is slower” is true on latency and irrelevant if your actual problem is 200,000 edge-function connections. The number that matters is the one measured at your concurrency level on your hardware.

Pricing and the real cost of ownership

The license cost of PgBouncer, PgCat, Supavisor, and Pgpool-II is zero — they’re all open source. The actual cost is operational: you run them, patch them, monitor them, make them highly available, terminate TLS, and wake up when they fall over. A single PgBouncer is a single point of failure, so production setups usually mean two or more behind a load balancer, plus health checks and alerting. That’s real engineering time, even if the binary is free.

RDS Proxy flips that. It’s $0.015 per vCPU-hour with a 2-vCPU minimum (regional rates run up to about $0.030), billed per second after a 10-minute minimum. For a provisioned instance that’s roughly $22/month per vCPU at the floor, so around $44/month minimum before traffic. Aurora Serverless v2 bills per ACU-hour instead. On top of that, extra proxy endpoints provision PrivateLink interfaces that carry their own charges. Not free, but compare it against the salary cost of someone keeping a self-hosted HA pooler healthy and it often pencils out — especially for smaller teams who’d rather not own the thing.

Supabase bundles Supavisor into its platform, so if you’re already on Supabase you’re effectively paying for it through your plan and there’s nothing extra to operate.

Which one should you actually pick

Skip the logos and start from your workload.

Traditional server app, moderate concurrency, self-hosted. PgBouncer. It’s the default for a reason — proven, light, and with prepared statements sorted in 1.21+, the old objections are gone. Don’t overthink it.

You need read/write splitting or sharding. PgCat. Pushing that routing out of your application code and into the pooler is cleaner, and it outperforms PgBouncer once you’re past ~50 concurrent clients anyway.

Serverless, edge functions, or multi-tenant SaaS. Supavisor. The connection counts that kill single-process poolers are exactly what it’s built for. The latency cost is the price of that scale, and for serverless it’s worth it. If you’re choosing serverless Postgres in the first place, our Neon vs PlanetScale vs Turso vs Supabase comparison pairs naturally with this decision.

Already all-in on AWS RDS or Aurora. RDS Proxy. The integration, IAM auth, and managed failover are worth more than the per-vCPU fee for most teams, and you delete an entire class of operational work.

You want pooling plus caching plus load balancing in one box. Pgpool-II — but go in with eyes open about the configuration complexity. If you only need pooling, the focused tools will serve you better with less to break.

One more thing worth saying: a pooler doesn’t fix an application that opens connections carelessly. If your ORM isn’t reusing connections or your Lambda handlers open a fresh connection per invocation outside the pooler, you’ll just move the bottleneck. How your data layer manages connections matters as much as the pooler in front of it — our Drizzle vs Prisma vs Kysely breakdown gets into how each handles this. And once you’ve got pooling sorted, the next thing that’ll ruin a weekend is backups — the Postgres backup tools comparison is worth a look while you’re thinking about production hardening.

Pick the pooler, then go set max_prepared_statements to 100 and re-run your load test. The throughput jump from that one line is the cheapest win you’ll get all quarter.

Sources: Tembo benchmark, pganalyze on PgBouncer prepared statements, Crunchy Data, PgBouncer 1.21 release notes, Amazon RDS Proxy pricing, Supavisor on GitHub