Demo, all content is generated
Question

Supabase 'remaining connection slots are reserved' but only since deploying to Vercel

Solved · 341 views · asked by wendy_l · edited

Locally fine. On Vercel, after a few minutes of traffic (maybe 30 users), I get:

PrismaClientInitializationError: FATAL: remaining connection slots are reserved for non-replication superuser connections

My DATABASE_URL is the connection string from Supabase settings, port 5432. Claude Code set up Prisma with a singleton in lib/db.ts so I thought connections were reused.

What I’ve tried

Checked the singleton, it is there. Restarted the Supabase project, works for a few minutes then the error comes back.

Comment
Which connection string exactly, the direct one (db.xxx.supabase.co:5432) or a pooler one? hannah_reyes · edited

3 answers

Marked as helpful by the asker
hannah_reyes · edited

The singleton helps within one function instance, but on Vercel you have many instances, each holding its own connections, straight to Postgres on 5432. Small Supabase instances only allow a limited number of direct connections, so 30 users can fill them.

Use the pooler for the app and keep the direct connection for migrations only:

# .env (Vercel)
DATABASE_URL="postgresql://postgres.abcd:[pw]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgresql://postgres.abcd:[pw]@aws-0-eu-central-1.pooler.supabase.com:5432/postgres"
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

Port 6543 is transaction mode: connections go back to the pool after each transaction. pgbouncer=true stops Prisma from using prepared statements, which transaction mode does not support.

Comment
Switched, redeployed, 2 hours of traffic and zero errors. What does connection_limit=1 do exactly? wendy_l · edited
Each function instance opens at most 1 connection to the pooler. Serverless instances handle one request at a time anyway, so more would just be idle. hannah_reyes · edited
Bookmarking this, the pgbouncer=true part bit me last month with 'prepared statement already exists'. pawel_z · edited
postgres_pete · edited

To see who is holding connections before and after the change:

select usename, application_name, state, count(*)
from pg_stat_activity
group by 1, 2, 3
order by 4 desc;

If you see dozens of idle connections from the same app, that's serverless instances each keeping their own. After switching to the pooler they should mostly disappear from this list.

Comment
Before: 58 idle from prisma. After: 4. Very satisfying query. wendy_l · edited
jb_supa · edited

If you use supabase-js (not Prisma) for most queries, those go over HTTP through the Supabase API and don't hold Postgres connections at all. Only the direct Postgres clients need the pooler setup.

Comment