Home

Fix this error · Servers and hosting

Fix: FATAL: sorry, too many clients already (Postgres and Prisma P2024)

Fix Postgres connection exhaustion in serverless: pooled strings for Supabase and Neon, Prisma 7 and 6 pool settings, one client per process.

Postgres has a fixed number of connection slots, and every one is taken. In a serverless app this usually happens because each function instance opens its own pool of connections. The fix that works most often is to connect through your provider's pooled connection string (Supabase transaction mode on port 6543, or Neon's -pooler host), keep each instance's pool small, and create one database client per process.

FactDetail
ErrorFATAL: sorry, too many clients already
Also seen asFATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute · Prisma P2024: Timed out fetching a new connection from the connection pool
SQLSTATE53300 (too_many_connections)
Where you see itServer logs, Vercel or other serverless function logs, your Postgres client
Usual causeMany serverless instances, each with its own pool, connecting directly to the database
Usual fixUse the pooled connection string and a small pool per instance
BugTape catches itYes, through the Node SDK, when the error reaches your framework or is left unhandled

What the error means

Postgres allows max_connections connections at once, and keeps a few of them for superusers and roles with reserved-connection privileges. When the ordinary slots run out, a new connection gets one of two errors:

  • sorry, too many clients already when every slot is in use.
  • remaining connection slots are reserved for roles with the SUPERUSER attribute when only reserved slots are left. Postgres 15 and earlier print remaining connection slots are reserved for non-replication superuser connections.

Prisma has its own pool in front of the database. In Prisma ORM 6 and earlier, a query that waits too long for a free connection in that pool fails with:

P2024: Timed out fetching a new connection from the connection pool. More info: http://pris.ly/d/connection-pool (Current connection pool timeout: 10, connection limit: 5)

P2024 means Prisma's own pool was busy. The database may still have free slots. In Prisma ORM 7, the pool belongs to the driver adapter (for example pg), and its timeout and size are set on the adapter.

Common causes

  1. Serverless instances connecting directly. Each function instance creates its own client and its own pool. During a traffic spike, many instances times the pool size is more than the database allows. Paused instances can keep their connections open.
  2. A new client per request. new PrismaClient() or new Pool() inside the handler opens a new pool for every request.
  3. Hot reload in development. Next.js reloads the module that creates the client, and each reload creates another client.
  4. Too many parallel queries. Promise.all over a large list sends more queries than the pool has connections, so the rest wait and time out (P2024).
  5. Connections that are never released. A pool.connect() without client.release() in a finally block, or a long transaction left open.
  6. A small database plan. Small instances have a low max_connections.

How to fix it

See who holds the connections

Connect with a direct (non-pooled) string, using a role that can see other sessions, and run:

SHOW max_connections;

SELECT usename, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY usename, application_name, state
ORDER BY count(*) DESC;

Many idle connections from your app user point to pools in serverless instances. Many idle in transaction connections point to transactions that are never committed.

Use your provider's pooled connection string

A pooler such as PgBouncer or Supavisor accepts many client connections and shares a small number of database connections between them.

Supabase. Use the shared pooler in transaction mode on port 6543 for serverless and edge functions. Supabase says transaction mode does not support prepared statements, so turn them off in your client.

postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@[POOLER-HOST]:6543/postgres

Neon. Add -pooler to the endpoint id in the host name. Neon runs PgBouncer in transaction mode.

postgresql://user:password@ep-cool-darkness-123456-pooler.us-east-2.aws.neon.tech/dbname?sslmode=require

Run migrations over the direct connection string. Transaction pooling breaks tools that need one session for the whole run.

Configure Prisma for a pooler

Prisma ORM 7. The runtime client uses the pooled string through a driver adapter, and the CLI uses the direct string from prisma.config.ts. Keep max small in serverless functions.

# .env
DATABASE_URL="postgresql://USER:PASSWORD@POOLER-HOST:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://USER:PASSWORD@DIRECT-HOST:5432/postgres"

// prisma.config.ts
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
  schema: 'prisma/schema.prisma',
  datasource: { url: env('DIRECT_URL') },
});

// lib/db.ts
import { PrismaClient } from '../prisma/generated/client';
import { PrismaPg } from '@prisma/adapter-pg';

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL, max: 3 });
export const prisma = new PrismaClient({ adapter });

Prisma recommends ?pgbouncer=true for Supabase Supavisor and for PgBouncer older than 1.21.0. For PgBouncer 1.21.0 or later, Prisma recommends leaving it off.

Prisma ORM 6. Set the pool size in the connection URL. Without an external pooler, Prisma's serverless advice is to start with connection_limit=1.

postgresql://USER:PASSWORD@HOST:5432/DATABASE?connection_limit=1

Create one client per process

Create the client once at module level, outside the handler. In development, store it on globalThis so hot reload reuses it:

// lib/db.ts
import { PrismaClient } from '../prisma/generated/client';
import { PrismaPg } from '@prisma/adapter-pg';

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL, max: 3 }) });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

Release connections and let Vercel close idle ones

With node-postgres, always release a checked-out client. On Vercel with Fluid compute, attachDatabasePool from @vercel/functions releases idle pool clients before the function suspends.

import { Pool } from 'pg';
import { attachDatabasePool } from '@vercel/functions';

export const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 3 });
attachDatabasePool(pool);

export async function getUser(id: string) {
  const client = await pool.connect();
  try {
    const { rows } = await client.query('SELECT * FROM users WHERE id = $1', [id]);
    return rows[0];
  } finally {
    client.release();
  }
}

Limit parallel queries

Run a large batch in small groups so it never needs more connections than the pool has:

for (let i = 0; i < ids.length; i += 5) {
  await Promise.all(ids.slice(i, i + 5).map((id) => prisma.order.update({ where: { id }, data: { synced: true } })));
}

How to catch it in production with BugTape

These errors happen on the server, so the Node SDK reports them. In Next.js 15 and later on the Node runtime, a Prisma or pg error thrown in a page, route handler, Server Action or middleware reaches onRequestError. In Express, add bugtape.expressErrorHandler() after your routes; it reports errors that end in a 5xx. A rejected query that nothing awaits is reported as an unhandled rejection. If you catch the error yourself and return a fallback, call bugtape.captureException(err) in the catch, or BugTape never sees it.

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') (await import('@bugtape/node')).init({ service: 'web' });
}

export async function onRequestError(...args: Parameters<typeof import('@bugtape/node').captureRequestError>) {
  if (process.env.NEXT_RUNTIME === 'nodejs') await (await import('@bugtape/node')).captureRequestError(...args);
}

The SDK replaces numbers in messages before sending, so repeats of the same error from the same code location group into one issue with a count, even when the pool size or timeout in the P2024 message differs. That count shows when the problem starts during a traffic spike. Each report carries the route pattern, such as POST /api/orders. Server reports have no replay or console output.

In the browser, the SDK reports a failed request only when the error response arrives within 8 seconds of the click that started it. A P2024 that waits out a 10-second pool timeout arrives after that window. Requests still running after 15 seconds are reported as hung.

Set release (the SDK reads VERCEL_GIT_COMMIT_SHA on Vercel) so a fix that does not hold reopens the issue as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the change. You review the pull request.

FAQ

Should I just raise max_connections?

Raising it lets more processes connect, but each connection uses database memory, and Prisma warns that this can hurt performance or exhaust the server's resources. On managed Postgres the value is often tied to the instance size. A pooler fixes the cause, which is many clients each holding connections.

What is the difference between P2024 and "too many clients already"?

too many clients already comes from Postgres when the database has no free slots. P2024 comes from Prisma's own pool when a query waited too long for one of that pool's connections. You can get P2024 while the database has free slots, for example when one function runs many queries in parallel.

Do I need ?pgbouncer=true?

With Prisma and Supabase Supavisor, Prisma recommends it. With PgBouncer 1.21.0 or later, Prisma recommends leaving it off. Either way, run Prisma Migrate over a direct connection, because the schema engine needs a single session.

Why does this happen in development?

Next.js hot reload re-runs the module that creates your client, and each run opens a new pool. Store the client on globalThis outside production so every reload reuses it.

Sources

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.