Skip to content

Backend

Connection Pooling — PgBouncer vs Supavisor for Serverless Postgres

Every serverless function that hits Postgres opens a new database connection. Thousands of functions running at once? You'll exhaust the Postgres connection limit (usually 100) in milliseconds. Connection pooling is the fix: a proxy sits between your app and the database, reuses connections, and kills your function's direct line. PgBouncer is the industry standard. Supabase now ships Supavisor. Here's how to pick one, configure it right, and the math behind connection limits.

🔌 ♻️

Your Postgres database has a finite number of connections. Rebuild Relief ships Netlify Edge Functions and serverless tasks that hammer the database with microscopically short-lived processes. Each process opens a connection. By the time you've spun up 50 concurrent functions, you've maxed the connection pool and the 51st function hangs. Spoiler: you don't buy a bigger Postgres server. You add a pooler — a proxy that sits between your app and the database, reuses connections, and saves the database from connection death. PgBouncer has been the standard for a decade. Supabase recently shipped Supavisor, their own pooler designed for serverless. Here's how they differ, when to use each, and the real config pattern.

Why Connection Pooling Matters in Serverless

Traditional apps (Node.js server, Python Django, Ruby Rails) spawn a handful of long-lived processes that each hold one persistent database connection. The database never sees more than 10–20 connections even under load, because the app reuses the same connection for every query.

Serverless flips the model. Each function invocation is a separate process. It boots, connects to Postgres, runs a query, shuts down. In Netlify Functions or AWS Lambda, you might have 100 concurrent invocations. Each one opens a fresh connection. Postgres sees 100 simultaneous connection attempts. The default connection limit is 100. You're at the ceiling on the first wave of traffic. The 101st invocation gets "too many connections" and fails.

The math:

  • Postgres default max_connections: 100
  • Superuser reserved: ~3
  • Usable connections: ~97
  • Concurrent Netlify Functions during peak: 50–200
  • Result: Functions start failing at 97 concurrent requests

A pooler sits in the middle. Your functions connect to the pooler (cheap, reusable), not directly to Postgres. The pooler holds a small number of persistent connections to Postgres (e.g., 20–50) and multiplexes queries across them. Now your 100 concurrent functions don't need 100 Postgres connections — they share the pooler's 20–50 and queue up. No more "too many connections" errors.

PgBouncer: The Industry Standard

PgBouncer is a lightweight C daemon that proxies Postgres connections. It's been the default connection pooler for over a decade. Battle-tested on millions of connections. If you've ever used Heroku, Railway, or Render, you've hit PgBouncer behind the scenes.

How it works:

Client 1 → PgBouncer → Postgres (connection reused)
Client 2 → PgBouncer → Postgres (connection reused)
Client 3 → PgBouncer → Postgres (connection reused)
...
Client 100 → PgBouncer → (queues for next free Postgres connection)

Modes of operation:

  • Session mode (stateful): PgBouncer keeps a connection open for the entire database session. Client disconnects, connection goes back to the pool. Works with everything — transactions, prepared statements, session variables — but holds connections longer.
  • Transaction mode (stateless): PgBouncer closes the connection after each SQL statement. The next statement gets a fresh connection from the pool. Multiplexes more aggressively but breaks transactions and session-level features (like temp tables).
  • Statement mode (experimental): PgBouncer closes after each statement like transaction mode but less tested. Skip it.

For serverless, transaction mode is the sweet spot: each function call is one transaction, the pooler closes it, connection returns to the pool. No wasted connections.

Supavisor: Supabase's New Pooler

Supabase recently shipped Supavisor, their own connection pooler built from scratch in Rust. It's designed for serverless: fast, low memory, integrated into the Supabase dashboard. You don't run a separate daemon — it's hosted.

Key differences from PgBouncer:

  • Hosted, not self-managed: Supabase runs it for you. No separate server to deploy or monitor.
  • Built for serverless: Supavisor is optimized for the serverless connection pattern (many short-lived clients, low latency).
  • Native to Supabase: Integrated with Auth, RLS, and Supabase logging. One dashboard for everything.
  • Less battle-tested: PgBouncer has 15 years of production history. Supavisor is newer (2024+).

Supavisor supports both session and transaction modes. The Supabase dashboard shows connection stats, pooler health, and lag. You toggle pooling on/off without changing your connection string.

PgBouncer vs Supavisor: When to Use Each

Use PgBouncer if:

  • You're running Postgres on your own hardware (not Supabase, not managed cloud).
  • You want maximum control and zero vendor lock-in.
  • You're already familiar with PgBouncer config (pgbouncer.ini).
  • You're running on Railway, Render, or another platform that provides PgBouncer as a built-in add-on.

Use Supavisor if:

  • You're on Supabase (Pro tier or higher).
  • You want zero operations: Supabase manages it for you.
  • You're running serverless functions (Netlify, Vercel, AWS Lambda) that hammer Postgres.
  • You want integrated monitoring and metrics in the Supabase dashboard.

The Aidxn pattern: Use Supavisor for serverless edge functions and Netlify Functions. Direct connection for long-running services (background jobs, scheduled tasks). Supavisor is overhead for a single persistent connection; PgBouncer / direct connections are fine there.

Connection Limit Math

Time to do the math on your actual limits.

Without pooling:

Max Postgres connections: 100
Reserved for superuser: 3
Usable: 97

Max concurrent functions: 97
If you exceed 97 concurrent invocations, functions fail with "too many connections"

With PgBouncer / Supavisor:

Max Postgres connections: 100
Reserved for superuser: 3
Usable: 97

Pooler size: 20 (configurable)
= Pooler holds 20 persistent connections to Postgres

Max concurrent functions: limited by pooler queue size
If you set queue size to 1000, 1000 concurrent functions can queue
The pooler serializes them across 20 Postgres connections

Real numbers for Rebuild Relief: Peak concurrent Netlify Functions during a storm-damage alert push: ~150. Postgres default limit: 100. Without a pooler, we fail. With Supavisor set to 25 persistent connections, the pooler queues the 150 functions and runs them across the 25 connections. We never hit "too many connections." Latency goes up (queue wait), but the system stays online.

Recommended pooler size: Start at max_connections / 4 for serverless. For Postgres default 100, set the pooler to ~20–25. Monitor CPU. If the pooler is busy, grow it. If it's idle, shrink it.

Setting Up Supavisor on Supabase

If you're on Supabase Pro or higher, Supavisor is available. Enable it in the dashboard.

Steps:

  1. Log into Supabase dashboard → your project.
  2. Go to "Database" → "Connection pooling".
  3. Toggle "Enable pooling" ON.
  4. Choose mode: "Transaction" (recommended for serverless).
  5. Supabase generates a new connection string with the pooler endpoint.
  6. Update your app to use the new pooler connection string (or keep both: pooler for functions, direct for long-running services).
// Direct connection (long-running services)
const directDb = new Pool({
  connectionString: 'postgres://user:pass@db.supabase.co:5432/postgres'
});

// Pooled connection (serverless functions)
const pooledDb = new Pool({
  connectionString: 'postgres://user:pass@db.supabase.co:6543/postgres' // Note: port 6543 (pooler)
});

// Use in serverless function
export async function handleRequest(req) {
  const result = await pooledDb.query('SELECT * FROM users WHERE id = $1', [123]);
  return result.rows[0];
}

Setting Up PgBouncer Manually

If you're running Postgres elsewhere (AWS RDS, VPS, your own hardware), deploy PgBouncer separately.

Example: PgBouncer on a small VPS

; /etc/pgbouncer/pgbouncer.ini
[databases]
myapp = host=db.example.com port=5432 dbname=postgres user=postgres password=secret

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3

listen_port = 6432
listen_addr = 0.0.0.0

log_connections = 1
log_disconnections = 1

Start PgBouncer, update your app's connection string from localhost:5432 to pooler-host:6432, done. Now your app connects to the pooler, not directly to Postgres.

Six FAQs

Does a pooler add latency?

Minimal. PgBouncer and Supavisor are designed for low overhead. Network round-trip from app → pooler → Postgres adds ~1-2ms. If your Postgres is on the same cloud region as the pooler, latency is negligible. Worth the tradeoff compared to connection exhaustion.

Will a pooler break my transactions?

In transaction mode, yes — PgBouncer closes the connection after each transaction. If you rely on session-level state (prepared statements, session variables, temp tables), you need session mode. Trade: session mode holds connections longer and pools less aggressively. For serverless, transaction mode is correct.

What happens if the pooler crashes?

Your app can't reach Postgres. Add a health check: if the pooler is down, fall back to a direct connection (slower, but not broken). Most teams run two PgBouncer instances behind a load balancer for high availability. Supabase handles this for you — if Supavisor fails, they've got redundancy.

Can I monitor pooler health?

Yes. PgBouncer exposes metrics: number of active connections, queued clients, pool utilization. Supavisor shows everything in the Supabase dashboard. Log into Supabase → Database → Connection pooling → Stats. Watch for high queue depth (means clients are waiting) and adjust pool size up if needed.

Do I need a pooler for every environment?

Production: yes. Staging: maybe. Local dev: no. Dev databases are tiny and you probably aren't spawning hundreds of connections. Keep your local connection string pointing directly to Postgres. Use pooler strings only for cloud functions and staging/production.

Does pooling work with Postgres read replicas?

Yes. Route writes to the pooler connected to the primary, reads to a separate pooler connected to replicas. Some poolers (like PgBouncer) can route queries based on read/write type. For Supabase read replicas, manage connection strings in your app code for now — Supavisor doesn't auto-route yet. See Database Read Replicas on Supabase for the full pattern.

The Bottom Line

Serverless functions + Postgres = connection pooling. PgBouncer is the proven classic — deploy it if you're running Postgres on your own. Supavisor is Supabase's new answer — zero ops, integrated, purpose-built for serverless. Both solve the same problem: finite connections, infinite function invocations. The Aidxn pattern is simple: Supavisor for serverless edge/Netlify functions, direct connection for long-running services. Start with a pool size of ~25, monitor queue depth, scale it up if clients are queueing. Your Postgres connection limit is no longer a blocker. Ready to architect your database access layer? Check Aidxn Design pricing for database performance consulting.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.