remaining connection slots are reserved for non-replication superuser connections
What it means
Postgres refused a new connection because the server is at or near its max_connections limit and the last free slots are held back for superuser use. Each connection is a separate backend process, so a migration that opens many direct connections (or leaks them) exhausts the pool fast. Route through the transaction-mode pooler on port 6543 and cap your client pool.
Why it happens
Postgres has a fixed max_connections. Every connection is a full backend process, and a few slots are held back via superuser_reserved_connections so an admin can always get in. When the free slots run out, a new non-superuser connection is rejected with SQLSTATE 53300. During a migration this happens for three reasons:
- Connecting directly instead of through a pooler. A direct connection to port 5432 consumes one real Postgres process for its whole lifetime. Supabase's smaller compute tiers cap direct connections in the tens, and the dashboard, PostgREST, and Realtime already hold some. A migration that opens a handful of parallel direct connections on top of that hits the ceiling.
- A client pool set too high. Node/
postgres.js,pg, and most drivers open a pool of connections. If yourmaxis 20 and you also run the app in another tab, you are asking for 20+ backends that may not exist. - Leaked connections. A connection that is created per batch and never closed (missing
await sql.end(), an unawaited query, an error path that skips cleanup) accumulates until the limit is reached. You will see the count inpg_stat_activityclimb and never drop.
How to fix it
First, confirm the cause. Connect as an admin (the reserved slots let you in even when the pool is full) and count what is open:
-- Total backends vs. the hard limit
select
(select count(*) from pg_stat_activity) as in_use,
current_setting('max_connections')::int as max_conn,
current_setting('superuser_reserved_connections')::int as reserved;
-- Who is holding connections, grouped
select usename, application_name, state, count(*)
from pg_stat_activity
group by usename, application_name, state
order by count(*) desc;
If you see many rows in state idle from your own migration user, those are leaks. Terminate the stale ones (keep your current session — that is pid = pg_backend_pid()):
select pg_terminate_backend(pid)
from pg_stat_activity
where usename = 'postgres'
and application_name = 'your_migration_app'
and state = 'idle'
and pid <> pg_backend_pid();
Then stop the root cause. Connect through the transaction-mode pooler, which multiplexes many sessions onto few backends. Use port 6543 and the pooler host, not the direct db.<ref>.supabase.co:5432 host:
# Transaction mode (pooler) — for short-lived queries, the migration default
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
Cap your client pool so it can never ask for more than the server allows. With postgres.js:
import postgres from 'postgres'
const sql = postgres(process.env.DATABASE_URL, {
max: 5, // hard ceiling on concurrent backends
idle_timeout: 20, // seconds: drop idle connections
connect_timeout: 30,
prepare: false, // required for the transaction-mode pooler
})
// ... run the migration ...
await sql.end({ timeout: 5 }) // always close when done
Note prepare: false: the transaction-mode pooler does not support prepared statements, so leaving it on produces its own errors. If you genuinely need session features (advisory locks, SET, LISTEN/NOTIFY), use session mode on port 5432 but keep the pool small — 2 or 3 connections.
How to prevent it
- Default to the pooler. Use port
6543for the migration's read/write queries. Reserve a direct5432connection only for the few operations that need a full session, and close it immediately. - Set an explicit, small pool
max. Pick a number below your tier's limit and belowmax_connections - reserved - other_services. For a migration, 5 concurrent connections is plenty because inserts are FK-ordered and batched, not massively parallel. - Always close. Wrap the run in
try/finallyand callsql.end(). Guarantee cleanup on the error path, not just the success path — leaked connections on a failed run are the usual reason a retry immediately hits the limit again. - Re-check
pg_stat_activityafter a failed attempt. Ifidleconnections from your last run are still open, terminate them before retrying, or bumpidle_timeoutdown so the server reaps them.
SupaMigrate runs its inserts through the transaction pooler with a fixed, small connection cap and closes every connection when a step finishes or errors, so a burst of batched inserts never maps to a burst of Postgres processes. See connection pooler for how transaction mode reuses backends. If your connection is failing to open at all rather than being refused for capacity, check password authentication failed and connect ENETUNREACH on an IPv6 address.
Frequently asked questions
- Is "remaining connection slots are reserved" the same as "sorry, too many clients already"?
- They are the same underlying condition — the server is at max_connections. Postgres returns "remaining connection slots are reserved for non-replication superuser connections" (SQLSTATE 53300) when only the superuser-reserved slots are left. Some clients and pgbouncer surface it as "sorry, too many clients already". The fix is identical: use a pooler and cap your pool size.
- How many connections does a Supabase project allow?
- It depends on the compute tier. A small instance defaults to roughly 60 direct connections, minus superuser_reserved_connections (default 3) and slots already used by the dashboard, PostgREST, and other services. Run "show max_connections;" on your own project to see the real number instead of assuming.
- Does connecting through the pooler fix this?
- Mostly. The transaction-mode pooler on port 6543 multiplexes many client sessions onto a small number of real backends, so a burst of client connections no longer maps one-to-one to Postgres processes. You still need to cap your client-side pool and close connections, but pooling removes the most common cause during a migration.
Related errors