Skip to content
All articles
Articlepostgrespoolingoperationsserverless

Connection Pooling for Modern Postgres: pgBouncer, Supavisor, PgCat

The 2026 state of Postgres connection pooling for serverless and traditional servers: pool modes, when transaction-mode breaks, prepared statements, and which pooler to pick.

14 min read

Postgres connections are heavy. Each one is a forked process with its own ~10MB of memory. Applications that open a connection per request die at 200 concurrent requests. The fix has always been a pooler, but in 2026 there are four credible options and the trade-offs have shifted, especially for serverless.

This is what we actually run, and why.

Why pool at all

Postgres's default max_connections is 100. Many managed providers cap it lower. A Next.js app on Vercel with even modest traffic can easily exceed that during a spike. Without pooling, your app hits "too many connections" and stalls until connections free up.

Pooling fronts your database with a separate process that:

  • Holds a small set of upstream connections to Postgres.
  • Accepts many client connections from your app.
  • Multiplexes client work over the upstream pool.

The crucial question is how the pooler decides which client gets which upstream connection. That's pool mode.

Pool modes: session, transaction, statement

Session mode (default)

A client gets a dedicated upstream connection for the lifetime of its session. When the client disconnects, the upstream is returned to the pool. Same as having no pooler, basically, just with reuse across client sessions.

Compatible with everything Postgres supports (prepared statements, session variables, advisory locks, LISTEN/NOTIFY). Doesn't solve the scaling problem because each client still holds a connection while idle.

Transaction mode

A client gets an upstream connection only for the duration of a transaction (or a single statement outside a transaction). At transaction commit, the connection returns to the pool and the next waiting client gets it.

This is what gives you the scaling win. 10 upstream connections can serve 1000 concurrent clients if their transactions are short.

The cost: session-scoped state breaks. You lose:

  • Prepared statements (without protocol-level de-duplication, the next transaction won't see the prepare).
  • Session variables (SET search_path, SET application_name are lost).
  • LISTEN/NOTIFY (sessions can't listen on a connection they don't keep).
  • Advisory locks held across transactions.

Statement mode

The pooler releases the upstream connection after every statement. Transactions are forbidden. Used by exactly nobody.

pgBouncer

The grandparent. Single-threaded C, rock-solid, runs on tiny VMs. Used at meaningful scale for two decades. The reference implementation of transaction pooling.

What pgBouncer is great at:

  • Latency: ~0.1ms overhead per query.
  • Resource footprint: a single 32MB pgBouncer handles 10k+ clients.
  • Predictability: it does one thing and it's well-understood.

Where it's showing its age:

  • Single-threaded. CPU-bound for very high QPS workloads (past ~50k queries/sec on a single instance).
  • No prepared statement handling: in transaction mode you need to disable client-side prepare.
  • Configuration via flat file. No dynamic pool reconfiguration; you reload to change a pool size.
  • No read replica routing.

Still our default for most self-hosted Postgres deploys. Reliable, boring, fast.

Supavisor

Supabase's pooler. Written in Elixir, multi-tenant out of the box, designed for the "Supabase project per customer" shape. The default pooler on every Supabase project.

What Supavisor brings:

  • Multi-tenant native. A single Supavisor cluster can pool for thousands of databases, each with its own pool config. This is genuinely a game-changer for the "database per tenant" pattern.
  • Prepared statement deduplication. Supavisor v2 (mid-2025) handles prepared statements at the protocol level, so you can leave prepare: true on in your ORM and still use transaction mode.
  • Connection upgrade per request: a request that needs session-scoped state can be promoted from transaction-mode to session-mode on the same connection.
  • Horizontal scaling: Erlang/OTP gives Supavisor the ability to run as a cluster across multiple nodes.

The trade-offs:

  • Higher per-query overhead than pgBouncer (1-2ms typically), because of the Erlang runtime.
  • Bigger memory footprint per instance.
  • Tied to Supabase's release cadence if you're using it on-platform; self-hosting it is possible but less common.

If you're on managed Supabase, you're already on Supavisor. Use the transaction-mode endpoint (port 6543) for serverless workloads and the session-mode endpoint (port 5432) for traditional servers that benefit from session-scoped state.

PgCat

Rust pooler, came out of Instacart and matured rapidly in 2024-2025. Multi-threaded, transaction mode by default, supports read replica routing and sharding.

Why pick PgCat:

  • Multi-threaded: scales further than pgBouncer on a single instance.
  • Read replica routing: routes SELECT to replicas, writes to primary. Configurable per role.
  • Sharding: built-in support for application-level sharding via a query rewrite layer.
  • Prepared statement support: similar to Supavisor, PgCat handles client-side prepares correctly.

Where it's less mature:

  • Configuration is more complex than pgBouncer's flat file. You need to think about pools, shards, and roles.
  • Smaller community. When something breaks, you're reading Rust source, not stack overflow answers.

We pick PgCat when we need read-replica routing or sharding. For flat Postgres workloads, pgBouncer is still simpler.

RDS Proxy

If you're on AWS RDS or Aurora, RDS Proxy is the bundled answer. It runs as a managed service, IAM-aware, transaction-mode pooling with prepared statement support.

It works. The cost is significant ($0.024/instance-hour per vCPU you provision, on top of the database). The latency is fine (~1-2ms overhead). The lock-in is real.

Use it if you're already on RDS and don't want to operate a pooler. Use it begrudgingly.

The serverless pooling problem

Serverless platforms (Vercel, Cloudflare Workers, Netlify) start and tear down compute frequently. Each cold start would naively open a fresh connection. Without a pooler, this saturates Postgres in minutes under any real traffic.

The shape that works:

  1. Your serverless function opens a connection to a transaction- mode pooler (Supavisor or pgBouncer in tx mode).
  2. The pooler holds a small set of upstream connections to Postgres (10-50 is usually enough).
  3. Your function runs its query in a transaction. At commit, the pooler returns the upstream connection.
  4. Your function ends. Its connection to the pooler closes. The upstream connection is unaffected.
serverless-config.tsts
// postgres-js + Supavisor transaction mode (Vercel example).
const sql = postgres(process.env.DATABASE_URL!, {
  max: 1,            // one connection per function invocation.
  prepare: false,    // critical for transaction-mode pooling on pgBouncer.
                     // (Supavisor v2 handles prepare itself, so you can flip
                     // this back to true if you point at it directly.)
  idle_timeout: 0,   // release immediately after the request.
});

The HTTP shortcut

Neon introduced "serverless driver" in 2023: a Postgres client that speaks HTTP/WebSocket instead of the wire protocol. No connection state. Every query is a one-off HTTP request to Neon's proxy, which fans out to a tiny per-database connection pool on its side.

For pure serverless workloads with simple queries (no transactions spanning multiple statements, no prepared statements), this is the cleanest answer. Supabase shipped a similar option in 2025.

For workloads that need real transactions or session state, you still want a pooler.

Picking one

Our defaults:

  • Managed Supabase project: Supavisor in transaction mode (port 6543). It's included; don't fight the platform.
  • Self-hosted Postgres + Vercel: pgBouncer in transaction mode, with prepare: false in your driver.
  • Self-hosted Postgres + read replicas: PgCat for the read/write split.
  • Database per tenant: Supavisor, its multi-tenant architecture is the killer feature.
  • Neon: their serverless driver for serverless workloads, their bundled pooler for traditional servers.

Whatever you pick, audit your prepared-statement story. The single most common "why is everything slow?" cause in a properly- configured pooler is client-side prepare hammering an upstream connection on every transaction. Look up your ORM's docs for the right flag.

And once you're past the pool decision, use the rest of your Postgres toolbox to actually see what's happening, connection counts in pg_stat_activity, queue depth in your pooler's admin console, slow queries in pg_stat_statements. The pool is a means; the database's health is the end. Tools like our SQL playground are useful here because you can run those introspection queries quickly with a read-only safety net.

Suparbase is an admin workspace for Supabase. Encrypted credentials, server-side proxy, RLS debugger, SQL playground, AI assistant with diff-confirmed writes. Free tier for solo projects.

Related articles