The error reads like a timeout, so the first instinct is to raise the timeout. That almost never helps. P2024 means a query waited pool_timeout seconds (10 by default) for a free connection in the pool and none came back. The pool is full; the question is why.
The math
Prisma sizes each pool at num_physical_cpus * 2 + 1 unless you set connection_limit. On a 2-vCPU function that is 5 connections. Serverless platforms run one pool per warm instance, so concurrency multiplies it.
// 2 vCPU → default pool of 5 per instance // 40 concurrent instances during a traffic spike const perInstance = 2 * 2 + 1; // 5 const instances = 40; const total = perInstance * instances; // 200 connections // Postgres max_connections on a small managed plan: often 100 // → instances 21..40 wait, then throw P2024 (or Postgres 53300)
When Postgres itself runs out first you see 53300 too_many_connections instead. Same root cause, different layer.
Fix 1 · One connection per instance
A serverless instance handles one request at a time, so it rarely needs more than one connection. Pin the pool and let the pooler do the fan-in.
DATABASE_URL="postgresql://app:***@db-pooler.example.com:6543/app?pgbouncer=true&connection_limit=1&pool_timeout=20" DIRECT_URL="postgresql://app:***@db.example.com:5432/app"
Fix 2 · Put a pooler in front
PgBouncer in transaction mode (or your host’s built-in pooler) multiplexes thousands of client connections onto a few dozen server connections. Keep directUrl for migrations: they need session features the pooler strips.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // pooled
directUrl = env("DIRECT_URL") // migrations only
}Fix 3 · Reuse the client across invocations
import { PrismaClient } from "@prisma/client"; const g = globalThis as unknown as { prisma?: PrismaClient }; export const prisma = g.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== "production") g.prisma = prisma;
Watch the ratio, not the number: alert when active connections exceed 80% of max_connections for five minutes. By the time P2024 shows up in Sentry you are already shedding requests.
What not to do
- Raising
pool_timeoutto 60s: requests now hang a minute before failing. - Raising
max_connectionsto 1000: each Postgres backend costs memory; you trade P2024 for OOM kills. - Calling
$disconnect()at the end of every handler: the next invocation pays the TLS handshake again.