How to diagnose Supabase connection exhaustion in Next.js
Distinguish pooler client limits from Postgres connections, inspect safely, and verify a bounded recovery.
A Supabase discussion about max connections illustrates the practical question: what is actually full? First identify which limit is exhausted. Max client connections reached can mean too many clients connected to the pooler; it is not proof that Postgres itself has reached max_connections. Supabase documents these as different limits.
Identify the connection path
Record the exact error, time, deployment and whether the application uses the direct database endpoint, shared pooler or dedicated pooler. Do not paste connection strings or passwords into logs or support tickets. Compare the application's deployment concurrency and per-instance pool size with the observed connection count; creating a pool for every request can multiply connections. An upper bound is instances multiplied by maximum connections per instance, plus migrations, jobs and other clients.
Supabase's connection guide distinguishes direct, shared session and shared transaction modes. Shared transaction mode reuses database connections between transactions and does not support prepared statements; verify your driver's settings. Shared session mode holds a database connection for the client session. The dedicated pooler has different behavior and supports transaction mode only. Check your current provider documentation and driver version before changing modes: session state and prepared-statement behavior are not interchangeable.
Inspect before changing settings
Use the Supabase dashboard's database and pooler metrics for the affected project. A pooler client count near its limit points to a different bottleneck than Postgres connections near the database limit. In the SQL editor, this aggregate shows visible Postgres sessions without printing query text or credentials:
select state, count(*) as connections
from pg_stat_activity
group by state
order by connections desc;This is a snapshot of database sessions visible to your role, not a count of all pooler clients. An idle in transaction session can hold resources; use the connection-management guide to investigate its owner and close the transaction cleanly. Do not terminate unknown sessions or restart shared services as a first step.
Reduce the source and verify recovery
To check a real client lifecycle locally, use a disposable local Postgres database and postgres.js, installed with npm install postgres. Set LOCAL_DATABASE_URL in your shell to that disposable database's connection string; do not print or commit it. Save this script as and run node pool-check.mjs:
import postgres from 'postgres';
if (!process.env.LOCAL_DATABASE_URL) throw new Error('Set LOCAL_DATABASE_URL');
const url = process.env.LOCAL_DATABASE_URL;
const sql = postgres(url, {
max: 2,
prepare: false,
connection: { application_name: 'pool-check-disposable' },
});
const observer = postgres(url, { max: 1, prepare: false });
let peak = 0;
try {
const work = Promise.all(Array.from({ length: 4 }, () => sql`select pg_sleep(0.5)`));
for (let attempt = 0; attempt < 20; attempt++) {
const rows = await observer`select count(*)::int as count from pg_stat_activity
where application_name = 'pool-check-disposable' and state = 'active'`;
peak = Math.max(peak, rows[0].count);
await new Promise((resolve) => setTimeout(resolve, 50));
}
await work;
if (![1, 2].includes(peak)) throw new Error(`Unexpected active peak: ${peak}`);
console.log(`Requests completed; observed active peak ${peak} (maximum 2).`);
} finally {
await sql.end({ timeout: 5 });
await observer.end({ timeout: 5 });
}After the process exits, use a separate connection to the same disposable database:
psql "$LOCAL_DATABASE_URL" -c "select count(*) from pg_stat_activity where application_name = 'pool-check-disposable';"Expect 0 sessions. Repeat the same four requests before and after correcting a real connection leak and compare the counts. This tests a local Postgres client pool and clean shutdown; it does not reproduce or verify hosted Supavisor limits. In an application, reuse one bounded pool per process and release explicitly checked-out clients in a finally block. In production, use your normal deployment procedure and observe both pooler and database metrics after the change; keep a rollback available.
If the pooler client limit remains the constraint, reducing connection multiplication or concurrency may help. Supabase says the shared pooler's maximum client connections is fixed by compute size; changing database pool size is a different setting. See pool settings before making a capacity decision. Do not assume increasing Postgres max_connections fixes a pooler client limit.
For a new CoreMVP application, the Postgres guide and database documentation explain the supplied foundation and its configuration. They are the place to check your actual application's connection setup before changing it.