Fix: Prisma Connection Pool Exhausted — Can't Acquire Connection from Pool
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Prisma connection pool errors — pool size configuration, connection leaks, serverless deployments, singleton pattern, query timeout, and pgBouncer integration.
The Problem
Prisma throws a connection pool error under load:
PrismaClientKnownRequestError:
Timed out fetching a new connection from the connection pool.
More info: http://pris.ly/d/connection-pool
(The pool timeout of 10s expired. Either increase the pool timeout or
increase the `connection_limit`)Or a serverless function runs out of database connections:
Error: Can't reach database server at `db.example.com:5432`
Please make sure your database server is running at `db.example.com:5432`.Or in development, connections accumulate and the database hits its max limit:
FATAL: sorry, too many clients already
(PostgreSQL max_connections: 100, current: 100)Or after deploying to Vercel/AWS Lambda, each function invocation opens new connections:
# 50 concurrent Lambda invocations × 10 connections each = 500 connections
# PostgreSQL max_connections = 100 → immediate exhaustionWhy This Happens
Prisma maintains a connection pool per PrismaClient instance. Each instance holds open database connections ready for queries. Connection pool exhaustion happens when demand for connections exceeds the pool size, or when the total connections across all instances exceed the database limit.
The problem manifests differently depending on your deployment environment. In a long-running server (Express, Fastify, NestJS), pool exhaustion usually means the pool is too small for the concurrency level, or long-running transactions are holding connections. In serverless environments (Lambda, Vercel Functions, Cloudflare Workers), the problem is fundamentally different: each cold start creates a new PrismaClient with its own pool, and the platform can spin up dozens of instances simultaneously, each opening fresh connections that the database wasn’t sized to handle.
Development environments have their own variant. Next.js and other frameworks with hot module reloading (HMR) destroy and recreate modules on each code change. Without a singleton pattern, every save creates a new PrismaClient instance that opens a new pool without closing the old one. After a few edits, you hit FATAL: sorry, too many clients already.
Common causes:
- Too many
PrismaClientinstances — eachnew PrismaClient()creates its own pool. In serverless environments, every cold start creates a new instance. - Pool too small for concurrency — the default pool size (
connection_limit) may be too small for the application’s concurrency level. - Connection leaks — queries that throw errors inside transactions may not release connections back to the pool if not handled correctly.
- Long-running transactions — transactions hold connections for their entire duration. Slow queries block connection release.
- Serverless cold starts — each Lambda/Vercel function invocation is a separate process. Without a connection pooler, each invocation opens a fresh pool.
Fix 1: Use a Singleton PrismaClient
The most important fix — create one PrismaClient instance shared across the application:
// lib/prisma.ts — singleton pattern
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
if (process.env.NODE_ENV !== 'production') {
// In development, Next.js hot reloads clear module cache
// Store on global to survive hot reload without creating new instances
globalForPrisma.prisma = prisma;
}
export default prisma;// usage — always import the singleton
import { prisma } from '@/lib/prisma';
export async function getUsers() {
return prisma.user.findMany();
}Verify only one instance exists:
// Add this to diagnose multiple instances during development
let instanceCount = 0;
export const prisma = new PrismaClient({
log: [{ emit: 'event', level: 'query' }],
});
instanceCount++;
console.log(`PrismaClient instance #${instanceCount} created`);
// If you see more than 1, you have multiple instancesFix 2: Configure Pool Size
Tune the connection pool size to match your workload and database limits:
// Set pool size in PrismaClient constructor
export const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});# Or via DATABASE_URL connection string parameters:
# ?connection_limit=5&pool_timeout=20
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=5&pool_timeout=20"# Connection string parameters:
# connection_limit — max connections in the pool (default: num_cpus * 2 + 1)
# pool_timeout — seconds to wait for a connection before timing out (default: 10)
# connect_timeout — seconds to wait for initial connection (default: 5)
# socket_timeout — seconds to wait for query result (default: none)Calculate the right pool size:
Recommended formula for shared databases:
connection_limit = (max_db_connections / num_app_instances) - reserved_for_migrations
Example:
PostgreSQL max_connections = 100
App instances = 4
Reserved = 5 (for migrations, admin, monitoring)
Per instance: (100 - 5) / 4 = ~23 connections// Dynamically set based on environment
const connectionLimit = process.env.NODE_ENV === 'production'
? parseInt(process.env.DB_POOL_SIZE ?? '10')
: 5; // Smaller pool in development
const databaseUrl = `${process.env.DATABASE_URL}?connection_limit=${connectionLimit}&pool_timeout=30`;
export const prisma = new PrismaClient({
datasources: { db: { url: databaseUrl } },
});Fix 3: Fix Serverless Deployments (Lambda, Vercel, Cloudflare)
Serverless functions create a new process per invocation, each with its own connection pool. The solution depends on your platform.
AWS Lambda — reuse PrismaClient across warm invocations:
// Lambda — keep PrismaClient alive between warm invocations
let prismaInstance: PrismaClient | null = null;
function getPrismaClient(): PrismaClient {
if (!prismaInstance) {
prismaInstance = new PrismaClient({
datasources: {
db: {
url: `${process.env.DATABASE_URL}?connection_limit=1&connect_timeout=15`,
},
},
});
}
return prismaInstance;
}
export async function handler(event: APIGatewayEvent) {
const prisma = getPrismaClient(); // Reuses existing instance on warm start
const users = await prisma.user.findMany();
return { statusCode: 200, body: JSON.stringify(users) };
}Vercel serverless functions — each function is isolated:
// Vercel functions share the Node.js module cache within a single
// function instance, but different functions (routes) run in separate
// processes. The global singleton pattern works within one function.
// pages/api/users.ts (Pages Router) or app/api/users/route.ts (App Router)
import { prisma } from '@/lib/prisma'; // Use the singleton
export async function GET() {
const users = await prisma.user.findMany();
return Response.json(users);
}
// For Vercel, set connection_limit=1 in DATABASE_URL
// Each function instance holds at most 1 connection
// Vercel spins up many instances — PgBouncer or Prisma Accelerate prevents exhaustionUse an external connection pooler (recommended for all serverless):
# PgBouncer — connect Prisma to PgBouncer instead of PostgreSQL directly
DATABASE_URL="postgresql://user:pass@pgbouncer-host:6432/db?pgbouncer=true&connection_limit=1"
# connection_limit=1 because PgBouncer handles pooling externally
# pgbouncer=true disables Prisma features incompatible with transaction pooling mode// With pgbouncer=true, Prisma disables:
// - Prepared statements (incompatible with transaction pooling mode)
// - Some advisory lock features
// This is expected behaviorPrisma Accelerate (hosted connection pooler):
# Replace DATABASE_URL with Prisma Accelerate URL
DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=YOUR_KEY"
# No code changes needed — Prisma Accelerate handles connection pooling
# Supports all Prisma featuresNeon with built-in pooling:
# Neon provides a pooled connection endpoint
# Use the pooled URL (port 5432) instead of the direct URL
DATABASE_URL="postgresql://user:[email protected]/db?sslmode=require&pgbouncer=true&connection_limit=1"
# Direct URL (for migrations only)
DIRECT_URL="postgresql://user:[email protected]/db?sslmode=require"Fix 4: PgBouncer Configuration Details
PgBouncer sits between your application and PostgreSQL, maintaining a shared pool of connections. Prisma requires specific PgBouncer settings to work correctly.
PgBouncer pool modes and Prisma compatibility:
| Pool Mode | Prisma Compatible | Notes |
|---|---|---|
transaction | Yes (with pgbouncer=true) | Recommended for serverless |
session | Yes | Acts like direct connection |
statement | No | Breaks multi-statement transactions |
PgBouncer configuration (pgbouncer.ini):
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 1000 # Max connections from Prisma instances
default_pool_size = 20 # Connections to PostgreSQL
reserve_pool_size = 5 # Extra connections under load
reserve_pool_timeout = 3Common mistake: forgetting ?pgbouncer=true in the Prisma connection string when using transaction pool mode. Without it, Prisma sends prepared statements that PgBouncer can’t route, resulting in cryptic prepared statement does not exist errors.
Fix 5: Prevent Connection Leaks
Connections leak when they’re not returned to the pool after use:
// Transactions — always handle errors to release the connection
// WRONG — error inside transaction may leak the connection
async function createUserWithProfile(data: CreateUserData) {
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: data.user });
throw new Error('Oops'); // Transaction rolled back, connection released — OK
// Prisma handles this correctly — transactions auto-rollback on throw
});
}
// Long-running transactions — avoid holding connections too long
// WRONG — fetching data inside transaction when it could be outside
async function processLargeDataset() {
await prisma.$transaction(async (tx) => {
const items = await tx.item.findMany(); // Holds connection while processing
for (const item of items) {
await expensiveExternalApiCall(item); // External call inside transaction!
await tx.item.update({ where: { id: item.id }, data: { processed: true } });
}
});
}
// CORRECT — do external work outside the transaction
async function processLargeDataset() {
const items = await prisma.item.findMany({ where: { processed: false } });
for (const item of items) {
const result = await expensiveExternalApiCall(item); // Outside transaction
// Short transaction just for the update
await prisma.item.update({
where: { id: item.id },
data: { processed: true, result },
});
}
}Explicit disconnect in scripts and tests:
// CLI scripts and one-off jobs — disconnect when done
async function main() {
try {
await runMigration();
} finally {
await prisma.$disconnect(); // Always disconnect when script ends
}
}
main().catch(console.error);
// Jest — disconnect after all tests
afterAll(async () => {
await prisma.$disconnect();
});Fix 6: Docker Container Connection Limits
Docker containers introduce an additional layer of connection management. Each container running your application maintains its own PrismaClient pool. With Docker Compose or Kubernetes, multiple replicas multiply the connection count.
# docker-compose.yml — 3 replicas × 10 connections = 30 connections
services:
app:
image: myapp
deploy:
replicas: 3
environment:
DATABASE_URL: "postgresql://user:pass@db:5432/mydb?connection_limit=10"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: pass
command: postgres -c max_connections=50
# 50 total − 3 reserved for superuser = 47 available
# 3 replicas × 10 = 30 — fits, but no room for growthCalculate limits for containerized deployments:
Available connections = max_connections - superuser_reserved
Per-container limit = Available / (replicas + migration_slots)
Example:
max_connections = 100
superuser_reserved = 3
replicas = 4
migration_slots = 2
Per container: (100 - 3) / (4 + 2) = ~16
Set connection_limit=15 (round down for safety)Kubernetes with HPA (auto-scaling) — the most dangerous scenario. If your HPA scales from 2 to 20 pods under load, and each pod has connection_limit=10, you suddenly need 200 connections. Use a PgBouncer sidecar or central pooler to prevent this:
# Kubernetes: PgBouncer as a sidecar container
spec:
containers:
- name: app
image: myapp
env:
- name: DATABASE_URL
value: "postgresql://user:pass@localhost:6432/mydb?pgbouncer=true&connection_limit=1"
- name: pgbouncer
image: bitnami/pgbouncer
env:
- name: POSTGRESQL_HOST
value: "postgres-service"
- name: PGBOUNCER_POOL_MODE
value: "transaction"
- name: PGBOUNCER_DEFAULT_POOL_SIZE
value: "5"Fix 7: Monitor Connection Pool Usage
Track active connections to diagnose pool exhaustion:
// Add metrics to PrismaClient
export const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'error' },
],
});
// Log slow queries (> 1 second)
prisma.$on('query', (e) => {
if (e.duration > 1000) {
console.warn(`Slow query (${e.duration}ms): ${e.query}`);
}
});Check active connections at the database level (PostgreSQL):
-- See all active connections to your database
SELECT
pid,
usename,
application_name,
client_addr,
state,
wait_event_type,
wait_event,
query_start,
query
FROM pg_stat_activity
WHERE datname = 'your_database_name'
ORDER BY query_start;
-- Count connections by state
SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = 'your_database_name'
GROUP BY state;
-- Idle connections holding pool slots
SELECT count(*) AS idle_connections
FROM pg_stat_activity
WHERE datname = 'your_database_name'
AND state = 'idle';Set statement timeout to prevent long-running queries from holding connections:
# DATABASE_URL with statement timeout
DATABASE_URL="postgresql://user:pass@host:5432/db?statement_timeout=30000"
# Queries taking more than 30 seconds are automatically cancelledFix 8: Handle Pool Timeout Errors Gracefully
Implement retry logic for pool timeout errors:
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
delayMs = 500,
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isPoolTimeout =
error instanceof PrismaClientKnownRequestError &&
error.message.includes('Timed out fetching a new connection');
if (isPoolTimeout && attempt < maxRetries) {
const delay = delayMs * Math.pow(2, attempt - 1); // Exponential backoff
console.warn(`Pool timeout, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// Usage
const users = await withRetry(() => prisma.user.findMany());Fix 9: Per-Environment Connection Strategy
Different environments need different pool configurations. A single DATABASE_URL across all environments is a common source of pool exhaustion.
// lib/prisma.ts — environment-aware configuration
import { PrismaClient } from '@prisma/client';
function buildDatabaseUrl(): string {
const base = process.env.DATABASE_URL!;
const url = new URL(base);
if (process.env.VERCEL) {
// Vercel serverless — minimal pool, use pooler
url.searchParams.set('connection_limit', '1');
url.searchParams.set('pool_timeout', '15');
url.searchParams.set('pgbouncer', 'true');
} else if (process.env.AWS_LAMBDA_FUNCTION_NAME) {
// Lambda — minimal pool, reuse across warm invocations
url.searchParams.set('connection_limit', '1');
url.searchParams.set('connect_timeout', '10');
} else if (process.env.NODE_ENV === 'production') {
// Long-running server — larger pool
url.searchParams.set('connection_limit', process.env.DB_POOL_SIZE ?? '10');
url.searchParams.set('pool_timeout', '30');
} else {
// Development — small pool
url.searchParams.set('connection_limit', '5');
}
return url.toString();
}
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
datasources: { db: { url: buildDatabaseUrl() } },
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}Still Not Working?
Prisma in Next.js App Router — each server component and API route in Next.js App Router runs in a separate module scope in development. Use the global singleton pattern (Fix 1) to prevent creating a new PrismaClient on every request in development hot reloads.
connection_limit vs database max_connections — connection_limit in Prisma’s URL sets the pool size per PrismaClient instance. The database’s max_connections is the total across all connections. If connection_limit * instances > max_connections, you’ll see “too many clients” errors.
Read replicas — if using read replicas, each connection string needs its own pool configuration. Prisma’s datasourceUrl override in queries can route read queries to replicas.
Prisma Migrate holding connections — running prisma migrate deploy opens its own connections outside the application pool. If your pool is already near the limit, migrations fail with connection errors. Reserve 2-3 connections in your pool calculation for migrations. Run migrations before scaling up application instances.
Supabase connection limits — Supabase free tier allows 60 direct connections. Use the Supabase connection pooler (Supavisor) URL instead of the direct connection URL. The pooler URL uses port 6543 and supports transaction mode:
# Direct (limited connections)
DATABASE_URL="postgresql://postgres:[email protected]:5432/postgres"
# Pooler (recommended for serverless)
DATABASE_URL="postgresql://postgres:[email protected]:6543/postgres?pgbouncer=true"For related issues, see Fix: Prisma Transaction Error, Fix: Prisma N+1 Query Problem, Fix: PostgreSQL Max Connections Exceeded, and Fix: Prisma Migration Failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Prisma Transaction Error — Transaction Already Closed or Rolled Back
How to fix Prisma transaction errors — interactive transactions vs $transaction array, error handling and rollback, nested transactions, timeout issues, and isolation levels.
Fix: Drizzle ORM Not Working — Schema Out of Sync, Relation Query Fails, or Migration Error
How to fix Drizzle ORM issues — schema definition, drizzle-kit push vs migrate, relation queries with, transactions, type inference, and common PostgreSQL/MySQL configuration problems.
Fix: Prisma Enum Not Working — Invalid Enum Value or Enum Not Recognized
How to fix Prisma enum errors — schema definition, database sync, TypeScript enum type mismatch, filtering by enum, and migrating existing enum values.
Fix: MongoDB Schema Validation Error — Document Failed Validation
How to fix MongoDB schema validation errors — $jsonSchema rules, required fields, type mismatches, enum constraints, bypassing validation for migrations, and Mongoose schema conflicts.