Skip to content

fix(db): cap the pg connection pool, configurable per environment - #71

Open
valentinpanizza wants to merge 1 commit into
diwenne:mainfrom
valentinpanizza:fix/bound-pg-connection-pool
Open

valentinpanizza wants to merge 1 commit into
diwenne:mainfrom
valentinpanizza:fix/bound-pg-connection-pool

Conversation

@valentinpanizza

Copy link
Copy Markdown

What happens

On a Vercel + Supabase deployment, the app and the worker intermittently fail with

Invalid `prisma.automation.findMany()` invocation
Can't reach database server at aws-0-<region>.pooler.supabase.com

which reads like the database being down. It is not — the connection pool is full.
Connecting from anywhere else at that moment returns:

FATAL: (EMAXCONNSESSION) max clients reached in session mode
       - max clients are limited to pool_size: 15

In my case it broke sign-in entirely (a magic-link request 500s), and the worker
logged "Can't reach database server" every few minutes for two days before I found
the real cause.

Root cause

lib/db/client.ts constructs the adapter with the connection string alone:

adapter: new PrismaPg(databaseUrl)

PrismaPg accepts a pg.PoolConfig, but given only a string it falls back to
pg.Pool's default of 10 connections per process, and Prisma's own
connection_limit URL parameter does not apply to the driver adapter — it is a
query-engine setting, so adding it to DATABASE_URL silently changes nothing.

On serverless that is per warm instance. Supabase's free session pooler allows 15
connections total, so two warm Vercel instances plus a long-running worker exhaust
it. Traffic does not need to be high: two instances is enough.

The fix

Pass a real PoolConfig:

adapter: new PrismaPg({
  connectionString: databaseUrl,
  max: Number(process.env.DB_POOL_MAX ?? 3),
  idleTimeoutMillis: 10_000,
})

DB_POOL_MAX is per-environment on purpose, because the two runtimes want
different values: the serverless app wants 1 (many short-lived instances), while
the single long-lived worker can use 3. The default of 3 keeps existing
self-hosted deployments closer to sane than the current implicit 10.

idleTimeoutMillis releases idle connections instead of holding them for the life
of the instance, which is what let a handful of warm lambdas sit on the whole pool.

Verification

With DB_POOL_MAX=1 on Vercel and 3 on the worker, the same deployment went from a
permanently exhausted pool (15/15, sign-in failing) to 4 of 15 at rest, and the
intermittent "Can't reach database server" errors stopped.

Nothing changes for anyone who does not set the variable beyond the pool being
bounded at 3 instead of an implicit 10.

PrismaPg was constructed with just the connection string, so pg.Pool fell
back to its default of 10 connections per process. On Vercel each warm
serverless instance builds its own pool, so two instances already exceeded
the 15 connections of Supabase's free session pooler. The failure surfaced
as Prisma's "Can't reach database server", which reads like the database
being down rather than its pool being full.

Pass a PoolConfig instead: max comes from DB_POOL_MAX (default 3) so the
serverless app and the long-lived worker can be tuned separately, and
idleTimeoutMillis releases idle connections instead of holding them.
@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

@valentinpanizza is attempting to deploy a commit to the diwenne's projects Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant