Search nomadLab

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

Picking a Postgres pooler is mostly picking a pooling mode and living with what it breaks. Five options, what each one is actually for, and the setting that stopped being a footgun.

Updated

If you got here from FATAL: sorry, too many clients already, the fix is a connection pooler and you already suspect that. The part that costs a weekend is the pooling mode, not the tool.

Postgres forks a full OS process per connection. Not a thread, a process, with its own memory before it has run a single query. Several hundred idle connections can hold gigabytes and burn CPU on context switches while doing nothing at all. Managed instances cap you accordingly, usually far lower than people expect, and then a serverless fleet scales to a few hundred concurrent executions that each want their own connection. The connections are not busy. They are just there, holding slots.

A pooler breaks the link between “the app wants a connection” and “Postgres spawns a backend.” Ten thousand clients can share fifty backends, if the workload allows it. Everything below is about that last clause.

Versions and defaults here were checked against vendor docs and the GitHub release APIs on 21 August 2026.

Modes, and what each one costs you

Session pooling hands a client one backend for the life of its connection. Nothing breaks, because nothing is shared. It also barely helps, since an idle client still squats on a backend.

Transaction pooling hands out a backend per transaction and takes it back at commit. This is where the multiplexing actually happens, and it is what you want in almost every case. It is also the mode that breaks applications, because your next transaction can land on a different backend.

Statement pooling returns the backend after every statement and forbids multi statement transactions outright. You will know if you need it.

In session pooling one client holds a backend for its whole connection; in transaction pooling three clients take turns on the same backend, one transaction each, so session state does not survive between them Session pooling Transaction pooling backend #7 belongs to one client, mostly while it sits idle backend #7 serves three clients, one transaction at a time client A: connect, query, idle, idle, query, disconnect A: one transaction B: one transaction C: one transaction At each dashed line the session resets: SET, LISTEN, temp tables, session advisory locks. SET LOCAL survives, because it belongs to the transaction rather than the session.
Everything transaction pooling breaks is something that assumed the connection stayed yours between transactions.

The list of casualties is short and predictable. Session level SET leaks or vanishes, so use SET LOCAL inside the transaction. Session scoped advisory locks do not stick, because a different backend never sees them. LISTEN and NOTIFY stop working once the listening backend is recycled. Session temporary tables disappear between transactions. None of that is the pooler’s fault. It is the price of multiplexing, and the tools differ only in how gracefully they tell you about it.

The prepared statement problem is now a version problem

For years the advice was to turn prepared statements off in transaction mode, which is why ORMs grew flags like Prisma’s pgbouncer=true and why you paid to re parse the same query forever.

PgBouncer 1.21 fixed it by tracking protocol level prepared statements and preparing them on whichever backend you land on. The part worth knowing in 2026: max_prepared_statements now defaults to 200, not 0. On a current PgBouncer the feature is on unless somebody turned it off. Writeups from pganalyze and Crunchy Data put the throughput gain anywhere from modest to several times, depending on how repetitive your query mix is.

So the real question is not whether to enable it. It is which PgBouncer you are running, and whether an old config file is still carrying max_prepared_statements = 0 from 2022.

The five, and what each is for

PgBouncer is the boring correct default and has been for over a decade. One small C binary, very little memory per client, transaction pooling done properly, and now prepared statements as well. Current release is 1.25.2, from May 2026. It does not do read/write splitting, sharding, or load balancing, and that refusal is most of why it is reliable.

PgCat is the Rust rewrite that adds replica routing, sharding, load balancing, and failover as configuration rather than application code. It is genuinely the better answer for that shape of problem, with one caveat that matters more than any benchmark: the repository’s last release is pgcat-0.2.5 from November 2024 and its last commit is from February 2025. That is eighteen months of nothing, on infrastructure that sits in the path of every query. Adopt it with that fact in front of you.

Supavisor is Supabase’s Elixir pooler, built for very large client counts across many tenant databases, and it scales horizontally in a way single process poolers do not. It is under active development, with v2.9.10 out in July 2026. It pays for that scale in per query latency, which is the correct trade when your problem is two hundred thousand edge function connections and the wrong one when your problem is fifty app servers.

RDS Proxy is the managed option if you already live on AWS, with IAM authentication and managed failover, priced per vCPU hour and billed per second after a ten minute minimum. Aurora Serverless bills per ACU hour instead, and extra proxy endpoints add PrivateLink charges. Rates vary by region, so read the number off the page rather than off a blog post.

Pgpool-II bundles pooling with load balancing, read/write splitting, query caching, and replication management. The breadth is the feature and also the problem: more to configure, more to misconfigure, more that can fail in a way that looks like a database bug.

Reading the benchmarks honestly

Tembo’s head to head is the most cited comparison, and its shape matters more than its numbers. Below roughly fifty concurrent clients, PgBouncer wins on latency and throughput because there is almost nothing in the path. Push past that and PgCat’s threading model pulls ahead and keeps climbing while PgBouncer flattens. Supavisor trails both on raw latency by a wide margin, and that is close to irrelevant for the workload it was built for.

Those runs are from someone else’s hardware, on someone else’s query mix, at a concurrency level you may never reach. Take the ranking as a hypothesis and measure at your own concurrency before you let it decide anything.

Picking one

Ordinary server side application, moderate concurrency, you run your own infrastructure: PgBouncer, and stop thinking about it. Check that prepared statements are on and move to the next problem.

Read/write splitting or sharding that currently lives in your application code: PgCat is the design you want, with the maintenance gap above as the thing to weigh. If that gap bothers you, the honest alternative is doing the routing in your application and keeping PgBouncer underneath.

Serverless functions, edge runtimes, or many tenant databases: Supavisor. Those connection counts are what kill the single process tools, and if you are still choosing the database underneath, the serverless Postgres options mostly ship a pooler of their own.

Already on RDS or Aurora with nobody who wants to own a highly available pooler: RDS Proxy. You are buying the deletion of an operational job, and for small teams that is usually worth more than the per hour rate.

Pooling plus caching plus routing in one process, and you have someone who will read the manual: Pgpool-II.

One thing a pooler will not do is rescue an application that opens connections carelessly. If your handlers open a fresh connection per invocation and bypass the pooler, you have moved the bottleneck rather than removed it, and how your data layer manages connections matters as much as what sits in front of the database. That part is on the ORM you picked and on you.

Before changing anything, connect to the PgBouncer admin console and run SHOW VERSION; and SHOW CONFIG;. Half the teams that think they have a pooling problem are running a five year old config file against a current binary.

Keep reading