TG
postgresql·pgbouncer·neon·10 min read

PgBouncer connection multiplexing, and how Neon does it

How PgBouncer multiplexing lets thousands of clients share a few Postgres connections in transaction mode, and how Neon builds it in with a serverless driver.

Ler em português
PgBouncer connection multiplexing, and how Neon does it

Connection multiplexing is how a pooler like PgBouncer lets thousands of clients share a handful of real Postgres connections, by lending a real connection only for the length of each transaction. It is the trick that keeps Postgres alive under serverless and autoscaling, where naive per-instance pools would blow past the connection limit. Neon builds the same idea into its platform with a managed PgBouncer and a serverless driver.

This post builds on How database connection pooling works. Read that first if terms like pool, session, and per-query borrowing are new. Here we go one level deeper: the mapping between client connections and server connections, and what you trade for it.

Why use a pooler at all?

Postgres has a hard connection ceiling set by max_connections, typically around 100. Every open connection is a backend process on the server, consuming around 3-5 MB of RAM even when idle.

Without a pooler, horizontal scaling hits this wall fast:

500 app instances × 10 connections each = 5,000 connections
Postgres: "too many clients already" → refuses new connections

With PgBouncer in front:

500 app instances → 5,000 connections to PgBouncer (cheap)
PgBouncer → only 20 real connections to Postgres
Postgres: fine, RAM preserved

PgBouncer multiplexes N clients onto a few real connections, so you can scale app instances horizontally without hitting the database ceiling.

Neon solves a different problem. Serverless functions (Vercel, Lambda, Cloudflare Workers) are ephemeral: each invocation can open and close a connection, and thousands of concurrent functions would overwhelm any Postgres. Neon addresses this three ways: a managed PgBouncer built into the platform, a serverless driver that speaks Postgres over HTTP and WebSocket (no TCP required), and scale-to-zero compute so you only pay while the database is active.

The rest of this post shows exactly how each works.

What is connection multiplexing?

Multiplexing means many logical clients sharing one physical resource over time. A pooler keeps two separate sets of connections: the ones facing your app (client connections) and the ones facing Postgres (server connections). The whole point is that the first set is large and cheap, and the second set is small and precious.

  clients (many, cheap)          PgBouncer            servers (few, precious)
  app conn 1 ─┐
  app conn 2 ─┤                ┌───────────┐          real conn A ── Postgres backend
  app conn 3 ─┼──────────────► │  maps N:1 │ ───────► real conn B ── Postgres backend
     ...      │                └───────────┘          real conn C ── Postgres backend
  app conn N ─┘

A client connection is almost free: it is just a socket to PgBouncer, which is a lightweight process. A server connection is expensive: it is a real Postgres backend process with its own memory. Multiplexing spends the cheap resource freely and rations the expensive one.

How does transaction mode map clients to connections?

In transaction mode, PgBouncer assigns a real server connection to a client only while a transaction is open, then takes it back the instant the transaction ends.

Walk through two clients sharing one server connection:

t0  client 1: BEGIN          -> gets server conn A
t1  client 1: INSERT ...        (holds A)
t2  client 1: COMMIT         -> releases A
t3  client 2: BEGIN          -> gets server conn A   (same one, now free)
t4  client 2: SELECT ...        (holds A)
t5  client 2: COMMIT         -> releases A

Between COMMIT and the next BEGIN, the client holds no server connection at all. An idle client, one that opened a connection but is not in a transaction, costs a socket to PgBouncer and nothing on Postgres. That is the multiplication: 5,000 mostly-idle clients can run on 20 real connections, because at any instant only a few are inside a transaction.

A single statement in autocommit is treated as a one-statement transaction, so it grabs a server connection, runs, and releases it in milliseconds.

What is the math behind the multiplication?

Two settings define the ratio:

SettingMeaningTypical value
max_client_connhow many clients may connect to PgBouncer5,000
default_pool_sizereal Postgres connections per user and database20

The first is huge, the second is small. The safe capacity is the same Little's Law from the pooling basics: real_connections = transactions_per_second × avg_transaction_seconds. If transactions are short (a few milliseconds), 20 real connections clear thousands of transactions per second, so 5,000 clients rarely wait. If transactions are long, the ratio collapses, because each one pins a real connection for its whole duration. This is why an open transaction that waits on application code (idle in transaction) is so damaging here: it holds a scarce server connection while doing no database work.

What does multiplexing cost you?

Because a client does not keep the same server connection between transactions, anything that lives in session state can break:

  • Driver-level prepared statements. PgBouncer 1.21+ supports them in transaction mode, but older setups do not, so you may need to disable them.
  • Session-scoped SET, session variables, and search_path tweaks meant to persist.
  • LISTEN / NOTIFY.
  • Advisory locks intended to be held across transactions.
  • Temporary tables expected to outlive a transaction.

Practical rules:

  1. Turn off driver-side prepared statements if you see errors (for example statement_cache_size=0, or the driver's simple-query option).
  2. Keep anything session-scoped inside one transaction.
  3. Use the direct (unpooled) connection for migrations, LISTEN/NOTIFY workers, and admin sessions.

How does Neon do pooling differently?

Neon is serverless Postgres, so pooling is not an add-on, it is part of the platform. Neon runs a managed PgBouncer in transaction mode in front of every database. You opt in by using the pooled host: Neon gives you two connection strings, and the pooled one has a -pooler suffix in the host.

direct   : postgres://user:pass@ep-cool-name.us-east-2.aws.neon.tech/db
pooled   : postgres://user:pass@ep-cool-name-pooler.us-east-2.aws.neon.tech/db

The pooled endpoint accepts far more connections than raw Postgres (Neon documents up to 10,000 on the pooled endpoint), which is exactly what serverless needs. Use the pooled string for the app, and the direct string for migrations and anything that needs a stable session. Neon also scales compute to zero when idle, so the pooler helps absorb the reconnection storm when a suspended database wakes up on the next request.

What is the Neon serverless driver?

Transaction pooling still assumes a normal TCP connection. Some environments, like edge runtimes, do not offer raw TCP at all. Neon's serverless driver (@neondatabase/serverless) solves this by speaking Postgres over HTTP and WebSocket instead of a raw socket:

  • HTTP (fetch): for a single, one-shot query, the driver sends it over an HTTP request. No connection to keep, ideal for stateless edge functions.
  • WebSocket: for transactions and sessions, it opens a WebSocket, which behaves like a connection for the length of the interaction.
edge / serverless function
        │  query over HTTP (one-shot)   ┌─────────────┐   ┌──────────┐
        └──────────────────────────────►│ Neon proxy   │──►│ Postgres │
        │  transaction over WebSocket    │ + PgBouncer  │   └──────────┘
        └──────────────────────────────►└─────────────┘

The mental model stays the same: multiplexing onto a small set of real connections. The driver just changes the transport so it works where a TCP pool cannot live.

// HTTP: one-shot query on the edge, no connection to keep
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL)
const rows = await sql`SELECT * FROM events WHERE id = ${id}`

Why does Neon price only on compute?

Because storage and compute are physically separate. Storage lives on an object store (similar to S3), which is cheap and billed per GB: almost negligible. What is expensive is the compute: the CPU and RAM of the running Postgres process.

Traditional managed Postgres charges for 24 hours of compute even if you use the database for 2. Neon charges only for the hours the compute is actually active.

traditional Postgres:  paid 24 h/day  ──  you used 2 h/day  →  wasted 22 h
Neon:                  paid 2 h/day   ──  compute slept the rest

For projects with uneven traffic (staging environments, MVPs, apps with nights and weekends idle), the savings are significant. For production with constant traffic, the models converge and the difference shrinks.

What about Railway, Render, and Supabase?

The pattern is identical across managed platforms, only the switch differs:

PlatformHow to get pooling
Railwayadd the PgBouncer plugin or service, point the app at its URL
Renderenable the connection pooler on the managed Postgres instance
Supabaseuse the pooler connection string (Supavisor, transaction mode) on port 6543
Neonuse the -pooler host, or the serverless driver on the edge

In every case the rule holds: app traffic goes to the pooler URL, and migrations plus session-bound work go to the direct URL.

PgBouncer vs Neon: what each one solves

They are complementary, not competing:

PgBouncerNeon
What it isOpen-source proxy and poolerManaged serverless Postgres
SolvesPostgres connection limit under many app instancesConnections in serverless and edge, plus infrastructure cost
You installYes, on your own infrastructureNo, built in
Scale-to-zeroNoYes
HTTP/WebSocket driverNoYes, for edge runtimes without TCP
When to useTraditional app (VPS, Railway, Render) with many instancesServerless, edge, pay-per-use projects

In practice: Neon includes a managed PgBouncer. If you use Neon, you already get pooling without installing anything. If you run Postgres yourself on Railway, Render, or a VPS, you consider adding PgBouncer separately.

Is Railway with PgBouncer the same as Neon?

Almost. Railway with PgBouncer solves the connection limit problem the same way Neon does. What is missing is scale-to-zero (Railway keeps Postgres running 24 hours regardless of traffic) and the HTTP/WebSocket driver for edge runtimes. For a traditional always-on app, Railway with PgBouncer gets you most of the way there. For serverless, irregular traffic, or edge, the gap matters.

TL;DR

  • Connection multiplexing lets many client connections share few real Postgres connections by lending one per transaction.
  • Transaction mode is what makes it work: a real connection is held only from BEGIN to COMMIT, so idle clients cost nothing on Postgres.
  • The ratio is set by max_client_conn (large) over default_pool_size (small), and it holds only while transactions stay short.
  • The cost is session state: prepared statements, LISTEN/NOTIFY, advisory locks, and temp tables can break, so keep session-scoped work in one transaction and use the direct connection for migrations.
  • Neon ships a managed PgBouncer (use the -pooler host) and a serverless driver that speaks Postgres over HTTP and WebSocket for edge runtimes.
  • Railway, Render, and Supabase expose the same pooling with a different switch. App to pooler, migrations to direct.

Written by AI, reviewed by Thiago Marinho

July 30, 2026 · Brazil