Postgres has a hard limit on simultaneous connections — typically 100 on a small instance, 500 on a medium one. A traditional Node.js server opens a pool of, say, 10 connections at startup and reuses them across all requests for the lifetime of the process. Serverless changes this contract completely.
The Serverless Connection Problem
In a serverless environment — Vercel Edge Functions, AWS Lambda, or Next.js API routes deployed on Vercel — each function invocation is a separate process. There is no shared connection pool between requests. Each cold start opens a new connection to the database; each function teardown closes it (or more often, forgets to close it). Under moderate traffic, you quickly see errors like:
FATAL: sorry, too many clients already
remaining connection slots are reserved for non-replication superuser connectionsThis isn't a code bug — it's a fundamental mismatch between the stateless serverless execution model and Postgres's stateful connection model.
What a Connection Pooler Does
A connection pooler sits between your application and Postgres. Your serverless functions connect to the pooler (which can handle thousands of simultaneous connections cheaply), and the pooler maintains a small, stable set of real Postgres connections that it multiplexes across requests.
- Transaction mode: a real Postgres connection is borrowed from the pool for the duration of a single transaction, then returned. This is the correct mode for serverless — supports thousands of app connections multiplexed over tens of real DB connections.
- Session mode: one real connection per client session. Useful for features that require session-level state (advisory locks, `SET LOCAL` settings, prepared statements). Not suitable for serverless due to high connection counts.
- Statement mode: most restrictive — each connection is returned after every statement. Only use when you don't need multi-statement transactions.
Warning
Transaction mode does not support session-level Postgres features: prepared statements, advisory locks, and `SET LOCAL` variables are reset after each transaction. If you use Prisma with transaction mode, set `pgbouncer=true` in your connection string and avoid prepared statements.
Supabase Supavisor: Built-in Pooling
Supabase replaced PgBouncer with Supavisor — their own Elixir-based connection pooler — in 2024. Supavisor supports both transaction mode (port 6543) and session mode (port 5432), and it's configured automatically for every Supabase project. You don't need to run or manage your own pooler.
# Direct connection (session mode — for migrations, long-running scripts)
postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres
# Pooled connection (transaction mode — for serverless / Next.js API routes)
postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=trueThe rule is simple: use the **pooled connection string (port 6543)** for your application runtime, and the **direct connection string (port 5432)** only for database migrations (where you need DDL statements and session-level features).
Configuring Prisma for Supavisor
Prisma requires two separate connection strings: one for migrations (direct) and one for the runtime query engine (pooled). You configure this in your `schema.prisma` and `.env`:
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // pooled — used at runtime
directUrl = env("DIRECT_URL") // direct — used for migrations
}# .env
DATABASE_URL="postgresql://postgres.[ref]:[pw]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgresql://postgres:[pw]@db.[ref].supabase.co:5432/postgres"Tip
Set `connection_limit=1` in your pooled DATABASE_URL when running in serverless. Prisma's internal pool becomes redundant when Supavisor is handling pooling — a limit of 1 prevents your function from trying to open multiple connections and exceeding the pool.
Monitoring Connection Usage
Supabase Dashboard → Database → Connection Pooling shows real-time connection counts. A useful Postgres query to check active connections yourself:
SELECT
count(*) AS total,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting
FROM pg_stat_activity
WHERE datname = 'postgres';Quick Checklist
- Use the pooled connection string (port 6543) for all Next.js API routes and Server Actions.
- Use the direct connection string (port 5432) only for `prisma migrate deploy` in CI/CD.
- Set `connection_limit=1` in the pooled DATABASE_URL if using Prisma.
- Add `?pgbouncer=true` to the pooled URL if using Prisma — required to disable prepared statements.
- Never commit either connection string to version control — use environment variables in Vercel project settings.
- Monitor pg_stat_activity if connections spike — check for missing `await` or unclosed DB clients in your code.