OpinionDatabasesInfrastructure

Postgres Still Needs a Windshield: On Built-In Pooling

Split-screen diagram showing 100 raw Postgres connections on the left versus PgBouncer routing 1000 clients through 20 pooled connections on the right

This week, Brandur Leach asked a simple question: does anyone run Postgres without PgBouncer? The Hacker News thread that followed collected 97 points and dozens of opinions before the day was out. The answer that emerged was essentially no — not for anything serious. That should bother us.

Everybody Bundles a Pooler

Brandur’s original post made a straightforward observation: he surveyed the major managed Postgres providers and found that virtually all of them ship with a connection pooler baked in. Supabase runs Supavisor, their own Elixir-based pooler. Neon includes managed PgBouncer on every endpoint. Railway, Render, and Fly.io all bundle one. Google Cloud SQL now offers managed connection pooling. Even AWS RDS has a proxy option — at extra cost.

One hundred percent of serious managed Postgres providers bundle a pooler. Not ninety. Not most. All of them.

Brandur compared this to buying a car without a windshield. The dealer knows you need one. You know you need one. And yet the car ships without it, and you have to source your own. It is a strange state of affairs for a database that has been in production use for three decades.

Why the Architecture Hits a Ceiling

Postgres uses a process-per-connection model. Every client connection spawns a dedicated OS process, consuming a minimum of 5–10 MB of RAM each, plus shared memory overhead. The default max_connections setting is 100. That number was reasonable in 2005. It is not in 2026.

Modern applications break this ceiling in predictable ways. A microservices deployment running 20 pods, each with a small in-app connection pool, can easily open 200 or 300 connections without a single user doing anything unusual. A serverless function that spins up 50 instances to handle a traffic burst will try to open 50 connections simultaneously. When Postgres hits that ceiling, it does not queue gracefully. It returns FATAL: sorry, too many clients already and refuses the connection entirely.

Every backend developer has seen this error at least once. Usually at an inconvenient hour. The fix — adding a pooler — works, but requires deploying new infrastructure, learning a new configuration surface, and debugging problems at a layer that sits between your application and your database.

MySQL handles this with native thread pooling. MongoDB handles it natively. Postgres does not.

PgBouncer Works. It Also Costs You.

PgBouncer is a solid piece of software. It has been around since 2007, it is stable, and in session mode it is largely transparent. The problem is that session mode is not what you actually want. Session mode returns a connection to the pool only when the client disconnects — which does not solve the over-connection problem. It just reorganizes it.

Transaction mode, which returns the connection after each commit or rollback, is far more efficient. It also breaks things. The list of features that do not work in transaction mode includes SET configuration, LISTEN/NOTIFY, advisory locks, and WITH HOLD cursors. PgBouncer 1.21 added partial support for prepared statements in transaction mode, but only for protocol-level prepares. Text-level PREPARE statements still fail. Reports of unnamed prepared statement does not exist errors with newer PgBouncer versions were still appearing in discussion threads as recently as February 2026.

ORMs handle this inconsistently. SQLAlchemy, Django, and Prisma each have their own workarounds. Some frameworks need DISCARD ALL as a server reset query. Others break silently in ways that are difficult to trace. One database provider employee commented in the HN thread that the biggest single source of their support tickets is customers using a PgBouncer connection for workloads it does not support.

None of this makes PgBouncer bad. It makes it an external dependency with a nontrivial operational surface — maintained separately, debugged separately, secured separately. That is a cost every team absorbs quietly because there is no alternative built into Postgres itself.

The Counterpoint Is Valid (To a Point)

Mike Bayer — the author of SQLAlchemy — noted in the thread that the vast majority of Postgres users outside managed cloud hosting are not using PgBouncer. He is right. A Django app backed by a single Postgres instance with a small in-app pool and a handful of concurrent users does not need a pooler. In-app pooling is sufficient, and adding PgBouncer to that workload adds complexity without benefit.

The problem is that the threshold where you do need it is lower than most teams expect, and you typically find out in production rather than in planning. Microservices, serverless functions, Lambda, anything with ephemeral compute — all of these hit the wall faster than a traditional monolith. And when they hit it, they hit it hard.

The Question Nobody Wants to Ask

The connection pooling problem is solved. Multiple production-grade solutions exist: PgBouncer, PgCat — a modern Rust-based pooler handling millions of queries per second in production — Supavisor, and Odyssey. The ecosystem has benchmarked all of them. The tooling is mature.

What has not happened is Postgres incorporating any of it. The PostgreSQL roadmap carries no proposal for native connection pooling. The maintainers have historically favored separation of concerns: Postgres should be a database, not an application server. That is a coherent position. It is also, in this specific case, a position that hands every production team a problem they must solve themselves, using a tool that Postgres itself does not fully support.

When every managed provider ships with a pooler and every serious team eventually adds one, the “it’s not Postgres’s job” argument starts to look less like a principle and more like inertia. The windshield is not optional. It should come with the car.

What to Actually Do Right Now

If you are running a classical application with persistent processes, start with in-app connection pooling. Set max_connections appropriately and tune your pool size. You may never need PgBouncer.

If you are running microservices, serverless, or anything with connection churn, add a pooler before the first incident rather than after it. PgBouncer in transaction mode with max_prepared_statements configured is the standard path. If you need multi-threading or read/write splitting, look at PgCat. If you are on a managed platform, use what the platform gives you and skip the extra infrastructure.

And if you are a Postgres maintainer reading this: the windshield analogy lands because it is accurate. The whole ecosystem has worked around the gap. At some point, working around the gap is evidence that the gap is worth closing.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *

    More in:Opinion