Skip to content

Fix: PostgreSQL "sorry, too many clients already"

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix PostgreSQL 'sorry, too many clients already' error — checking active connections, using connection pooling with PgBouncer, tuning max_connections, fixing ORM pool settings, and finding connection leaks.

The Error

PostgreSQL refuses new connections with:

FATAL: sorry, too many clients already

Or in your application:

Error: sorry, too many clients already
    at Connection.parseE (/app/node_modules/pg/lib/connection.js:604:11)

# Python/psycopg2
psycopg2.OperationalError: FATAL:  sorry, too many clients already

# Java/JDBC
org.postgresql.util.PSQLException: FATAL: sorry, too many clients already

The application can’t open new database connections because PostgreSQL has reached its max_connections limit.

Why This Happens

PostgreSQL has a hard limit on concurrent connections set by the max_connections configuration parameter (default: 100). Each connection consumes shared memory (~5-10 MB) regardless of whether it’s idle or active. Unlike a web server that can queue thousands of waiting requests, PostgreSQL allocates a dedicated backend process per connection. That means 200 connections equals 200 OS-level processes, each with its own memory allocation, file descriptors, and CPU scheduling overhead.

The problem compounds in microservice architectures. Each microservice runs its own connection pool, so five services with three replicas each, each holding a 20-connection pool, add up to 300 connections before a single admin query runs. Serverless environments make this worse because every function invocation may spin up a fresh connection that outlives the request.

Common triggers include: no connection pooling (each application instance opens its own pool, and 5 servers x 20 connections = 100 before accounting for migrations or admin tools), connection leaks (missing finally blocks or forgotten client.release() calls), ORM pool misconfigured (Sequelize, TypeORM, or Prisma pool set too high per process), serverless/edge functions (each invocation opens a new connection), long-running transactions that hold connections open indefinitely, and max_connections too low for the production topology.

Step 1: Check Current Connection Count

Before changing anything, see the current state:

-- Total connections by state
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count DESC;

-- Who's connected and what they're running
SELECT
  pid,
  usename,
  application_name,
  client_addr,
  state,
  query_start,
  state_change,
  left(query, 80) AS query
FROM pg_stat_activity
WHERE datname = 'your_database_name'
ORDER BY query_start;

-- Connection count vs limit
SELECT count(*) AS current,
       (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max
FROM pg_stat_activity;

If most connections are idle, you have a pooling problem — connections are open but not doing work. If many are idle in transaction, you have uncommitted transaction leaks.

How Other Databases and Poolers Handle This

Every relational database faces the same fundamental problem — OS-level processes or threads are expensive — but they solve it differently.

MySQL also uses a max_connections setting (default: 151), but each connection is a thread rather than a process. Threads share memory, so MySQL can handle more connections at lower memory cost per connection. The trade-off is that thread contention becomes the bottleneck under high concurrency instead of memory. MySQL’s thread_pool plugin (available in MySQL Enterprise and MariaDB Community) multiplexes connections similarly to a pooler, keeping a fixed number of worker threads regardless of client count. SQL Server uses a thread pool internally and has no hard max_connections equivalent in practice — its default is 32,767. Connection pooling is handled by ADO.NET or the JDBC driver on the client side, not by a separate proxy.

Connection pooler comparison: PgBouncer is the most widely deployed PostgreSQL pooler. It’s single-threaded, lightweight (~2 MB RSS), and handles 10,000+ client connections with minimal overhead. PgCat is a newer Rust-based pooler that supports multi-threaded connection pooling, sharding, and load balancing across read replicas in a single binary — useful if you need replica-aware routing without a separate HAProxy layer. Odyssey (from Yandex) is multi-threaded and designed for very high concurrency deployments (100K+ clients), but has a smaller community and less tooling. AWS RDS Proxy is a managed pooler that integrates with IAM authentication and handles failover transparently — it adds latency (~1-3 ms per query) but eliminates operational overhead. ProxySQL is MySQL-focused but worth noting: it does query routing, caching, and connection multiplexing in a single layer, which is something PostgreSQL typically needs PgBouncer plus a load balancer to replicate.

ORM-level connection pools also differ. SQLAlchemy (Python) uses a QueuePool with configurable pool_size and max_overflow — overflow connections are created on demand and destroyed after use. Prisma (Node.js) manages a pool internally and exposes a connection_limit URL parameter. HikariCP (Java) is considered the fastest JVM connection pool and is the default in Spring Boot — it validates connections on borrow, evicts idle connections, and reports pool saturation metrics via JMX. In all cases, the ORM pool sits between application code and the database (or pooler), so you end up with two levels of pooling if you also run PgBouncer. Sizing them correctly requires understanding the multiplication: 4 app instances x 10 ORM pool connections = 40 connections hitting PgBouncer, which then maps them to 20 actual PostgreSQL backend connections.

Fix 1: Add a Connection Pooler (PgBouncer)

PgBouncer sits between your app and PostgreSQL, multiplexing many application connections into fewer actual database connections. This is the most scalable fix for production:

# Install on Ubuntu/Debian
sudo apt install pgbouncer

# Config file: /etc/pgbouncer/pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# Pool mode: transaction is most efficient
pool_mode = transaction

# Max connections PgBouncer will open to PostgreSQL
server_pool_size = 20

# Max clients that can connect to PgBouncer
max_client_conn = 1000

# Connections per user+database combination
default_pool_size = 20
# userlist.txt — hashed passwords
"myuser" "md5<hash>"

# Generate hash
echo -n "passwordmyuser" | md5sum | awk '{print "md5" $1}'

Your application connects to PgBouncer on port 6432 instead of PostgreSQL on 5432. PgBouncer handles 1000 client connections using only 20 actual PostgreSQL connections.

Pool modes:

ModeDescriptionUse case
sessionOne server connection per client sessionLong-lived connections
transactionServer connection held only during a transactionMost web apps
statementReleased after each statementSimple read workloads

transaction mode is the most efficient for web applications — each connection is only held during an active transaction.

Note: In transaction pool mode, PostgreSQL features that depend on session state (prepared statements, SET LOCAL, advisory locks, LISTEN/NOTIFY) don’t work reliably. Disable prepared statements in your ORM when using PgBouncer in transaction mode.

Fix 2: Configure ORM Connection Pool Correctly

Many ORMs open too many connections by default. Configure the pool size based on your PostgreSQL max_connections:

Rule of thumb: pool_size per process = (max_connections - reserved) / num_app_instances

For example, with max_connections = 100, 10 reserved for admin tools, and 4 app instances: (100 - 10) / 4 = 22 connections per instance

Prisma:

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Connection pool URL format:
  // ?connection_limit=10&pool_timeout=30
}
// Or via DATABASE_URL
// DATABASE_URL="postgresql://user:pass@host/db?connection_limit=10&pool_timeout=30"

const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
  log: ['warn', 'error'],
});

Sequelize:

const sequelize = new Sequelize(DATABASE_URL, {
  dialect: 'postgres',
  pool: {
    max: 10,        // Maximum connections
    min: 2,         // Minimum connections kept open
    acquire: 30000, // Max time to wait for a connection (ms)
    idle: 10000,    // Time before idle connection is closed (ms)
  },
});

TypeORM:

createConnection({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  extra: {
    max: 10,           // Pool size
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
  },
});

node-postgres (pg) directly:

import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,                    // Max connections in pool
  idleTimeoutMillis: 30000,  // Close idle connections after 30s
  connectionTimeoutMillis: 5000, // Fail if no connection available in 5s
});

// ALWAYS release connections back to the pool
async function query(sql: string, params?: any[]) {
  const client = await pool.connect();
  try {
    return await client.query(sql, params);
  } finally {
    client.release();  // ← Critical: missing this causes connection leaks
  }
}

Fix 3: Fix Connection Leaks

If connections are accumulating even with a pool configured, there’s a connection leak — code that acquires connections but doesn’t release them.

Find long-lived idle connections:

SELECT pid, usename, application_name, state,
       now() - state_change AS idle_duration,
       left(query, 80) AS last_query
FROM pg_stat_activity
WHERE state = 'idle'
  AND state_change < now() - interval '10 minutes'
ORDER BY idle_duration DESC;

Kill leaked connections:

-- Kill a specific connection by PID
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE pid = 12345;

-- Kill all idle connections older than 30 minutes
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
  AND state_change < now() - interval '30 minutes'
  AND pid <> pg_backend_pid();

Common leak patterns in Node.js:

// WRONG — no finally block; connection leaked on error
async function badQuery() {
  const client = await pool.connect();
  const result = await client.query('SELECT ...');  // If this throws, release() is never called
  client.release();
  return result;
}

// CORRECT — release in finally
async function goodQuery() {
  const client = await pool.connect();
  try {
    return await client.query('SELECT ...');
  } finally {
    client.release();
  }
}

// SIMPLEST — use pool.query() directly (auto-releases)
async function simpleQuery() {
  return pool.query('SELECT ...');  // Pool manages connect/release automatically
}

Fix 4: Handle Serverless Functions

In serverless environments (AWS Lambda, Vercel Functions, Cloudflare Workers), each invocation may create a new connection. With concurrent requests, this quickly exhausts max_connections.

Use a managed connection pooler — AWS RDS Proxy, Supabase Pooler (PgBouncer), or Neon’s built-in pooling.

Or reuse the pool across invocations (warm Lambda instances):

// db.ts — module-level pool, shared across invocations in the same Lambda instance
import { Pool } from 'pg';

let pool: Pool | null = null;

export function getPool(): Pool {
  if (!pool) {
    pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 2,  // Keep small — multiple Lambdas run concurrently
      idleTimeoutMillis: 30000,
    });
  }
  return pool;
}

Use Prisma’s Accelerate or Data Proxy for serverless Prisma deployments — it acts as a connection pooler at the edge.

Fix 5: Increase max_connections

If you’ve implemented pooling and still hit the limit, increase max_connections in postgresql.conf:

# Find postgresql.conf
psql -c "SHOW config_file;"

# Edit the file
sudo nano /etc/postgresql/16/main/postgresql.conf
# Default is 100
max_connections = 200

# Increase shared_buffers proportionally (required)
# Roughly: shared_buffers = total_ram / 4
shared_buffers = 256MB
# Restart PostgreSQL (requires restart, not just reload)
sudo systemctl restart postgresql

Caveats: Each connection requires ~5-10 MB of shared memory. Increasing max_connections to 500 requires ~2.5-5 GB RAM just for connection overhead. More connections also increases lock contention and context switching. Pooling is almost always preferable to simply raising the limit.

Still Not Working?

Check for long-running idle transactions:

SELECT pid, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY duration DESC;

idle in transaction connections hold locks and prevent vacuuming. Add statement timeouts to auto-rollback stuck transactions:

-- Set in postgresql.conf or per-session
SET idle_in_transaction_session_timeout = '5min';

Check reserved connections — PostgreSQL reserves superuser_reserved_connections (default: 3) connections for superuser access. If max_connections = 100 and all 100 are taken, only a superuser can connect. This is why you’re still locked out even after terminating connections — your app user can’t use the reserved slots.

Inspect connections from external tools. Monitoring agents (Datadog, pganalyze, pg_stat_monitor), migration runners (Flyway, Alembic), and cron-triggered scripts all consume connections. Run SELECT usename, application_name, count(*) FROM pg_stat_activity GROUP BY 1, 2 ORDER BY 3 DESC; to identify which applications are consuming the most connections. A single misconfigured monitoring agent polling every 5 seconds with a fresh connection each time can silently consume dozens of slots.

Check pg_hba.conf connection limits per role. PostgreSQL supports CONNECTION LIMIT at the role level. If a role has ALTER ROLE myuser CONNECTION LIMIT 10; set, that user can’t open more than 10 connections even if max_connections has headroom. Check with SELECT rolname, rolconnlimit FROM pg_roles WHERE rolconnlimit > 0;.

Monitor connection count continuously:

-- Create a view for easy monitoring
CREATE VIEW connection_stats AS
SELECT
  max_conn,
  used,
  res_for_super,
  max_conn - used - res_for_super AS available
FROM (
  SELECT count(*) used FROM pg_stat_activity
) t1,
(SELECT setting::int AS max_conn FROM pg_settings WHERE name = 'max_connections') t2,
(SELECT setting::int AS res_for_super FROM pg_settings WHERE name = 'superuser_reserved_connections') t3;

SELECT * FROM connection_stats;

For related database issues, see Fix: Postgres Connection Refused, Fix: MySQL Too Many Connections, Fix: Prisma Connection Pool Exhausted, and Fix: AWS RDS Connection Timed Out.

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