Skip to content

Fix: Prisma Connection Pool Exhausted — Can't Acquire Connection from Pool

FixDevs · (Updated: )

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 exhaustion

Why 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 PrismaClient instances — each new 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 instances

Fix 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 exhaustion

Use 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 behavior

Prisma 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 features

Neon 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 ModePrisma CompatibleNotes
transactionYes (with pgbouncer=true)Recommended for serverless
sessionYesActs like direct connection
statementNoBreaks 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 = 3

Common 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 growth

Calculate 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 cancelled

Fix 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_connectionsconnection_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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles