← blog

Supabase 'max clients reached in session mode' on Vercel, and the fix that made it worse

September 20, 2026 · 7 min read

The error is (EMAXCONNSESSION) max clients reached in session mode - max clients are limited to pool_size: 15. Our API started returning it while the marketing site stayed perfectly healthy, because the marketing site does not touch Postgres. That is exactly why this class of failure goes unnoticed. The parts that look fine are the parts that do not need the database.

Here is what the error means, the two mistakes we made fixing it, and the connection string that actually works from Vercel.

Two poolers, two ports

Why serverless exhausts session mode

Each warm function instance holds its own small pool. Ours held three. A pool in session mode never gives connections back while the instance is warm. Five warm instances is fifteen connections, which is the cap. The sixth request from anywhere fails.

A fleet daemon polling three endpoints every few seconds kept instances warm around the clock. Nothing in that is unusual. It is just arithmetic that session mode loses.

Mistake one: fixing the wrong layer first

The emergency fix was max: 1 per instance and a twenty second idle timeout, still on session mode. It worked. Instances that went quiet gave their slot back, and the API recovered within minutes. We then moved the connection string to transaction mode, which was the right thing to do. We left max: 1 in place, which was not.

Mistake two: a pool of one on transaction mode

On transaction mode a single connection per instance serialises every query in that instance behind one pipe. The daemon's polling queued up behind it, requests exceeded their twenty second deadline, and the symptom changed from 500s to hangs. The marketing site was still fast. The API stopped answering at all.

Transaction mode returns a connection after each transaction, so it wants a normal-sized pool. We set eight and the timeouts stopped.

How to see it coming

The pooler tells you nothing until it refuses you. Postgres itself will, if you ask.

select application_name, state, count(*)
from pg_stat_activity
where datname = 'postgres'
group by 1, 2
order by 3 desc;

Run that on a session connection while the app is under normal load. If the count is creeping toward the cap and most rows are idle, you are one busy minute away from the error. On transaction mode the same query shows connections being recycled instead of hoarded.

The connection string that actually works

postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
  • Host is the pooler, not db.<ref>.supabase.co. The Supabase dashboard may show the direct host under a transaction pooler heading. It is still the direct host.
  • Username is postgres.<project-ref> on the pooler. Plain postgres works only on the direct host.
  • The direct host is IPv6 only unless you buy the IPv4 add-on. It has an AAAA record and no A record. Vercel functions cannot reach it, and port 6543 is not open there anyway.
  • No query string needed for postgres.js. Set prepare: false. The ?pgbouncer=true flag is a Prisma convention.
  • Keep migrations on 5432. Drizzle and similar tools want a session connection. Point the deployed app at 6543 and your local migration env at 5432.
$ dig +short A    db.<project-ref>.supabase.co     # (nothing)
$ dig +short AAAA db.<project-ref>.supabase.co     # 2600:1f18:...
$ nc -z aws-0-us-east-1.pooler.supabase.com 6543   # succeeded

Sizing, in one table

One last thing worth knowing. While all this was happening, the daemon that could not reach the API concluded that every machine it managed was dead. It prepared to power cycle them. That story is in how a database connection limit almost rebooted my customers' Macs.

Questions

Which Supabase port should a Vercel app use?
6543, the transaction-mode pooler. Session mode on 5432 holds one server connection per client and caps the project at 15, which serverless exhausts quickly.
Why does db.<project-ref>.supabase.co not connect from Vercel?
The direct host has only an IPv6 address unless you pay for the IPv4 add-on, and Vercel functions do not reach it over IPv6. Use the pooler host, aws-0-<region>.pooler.supabase.com.
What username does the pooler need?
postgres.<project-ref>, with the project ref appended. Plain postgres works only on the direct host.
Do I need ?pgbouncer=true in the connection string?
For Prisma, yes. For postgres.js, set prepare: false in the client options instead; transaction mode does not support prepared statements.
What pool size should each mode use?
Session mode: one or two per instance with a short idle timeout, because the cap is fifteen. Transaction mode: a normal pool, five to ten, because connections are returned after every transaction.

Run your own numbers on the calculator or lease a runner.