- English
- English
Appearance
FATAL: sorry, too many clients alreadyThe error message above is one of the most common issues that causes engineers to panic in the middle of the night during on-call shifts. This article explains what really happens behind the scenes of a database connection, why creating connections is expensive, and how to manage them properly so this error never happens in your system.
A database connection is a two-way communication channel between an application and a database server. Before any query can run, the application must open a connection first. The concept is similar to placing a phone call and waiting for the other person to answer before you can speak.
A detail that is often forgotten at work: creating a new connection is expensive.
Expensive here does not mean money. It means expensive in terms of time (latency) and hardware resources (CPU and memory). Misunderstanding why connections are expensive is often the root cause of major performance problems in production.
When an application calls a function like createConnection() or similar through a database driver, several steps happen before the first query can be sent:
In detail, the connection steps include:
Opening a TCP socket: the application opens a network connection to the database port. Common default ports include:
| Database | Default Port |
|---|---|
| PostgreSQL | 5432 |
| MySQL / MariaDB | 3306 |
| SQL Server | 1433 |
| Oracle | 1521 |
| MongoDB | 27017 |
Protocol handshake: client and server exchange protocol versions, capability information, and session parameters.
Authentication: the server verifies client identity using a username, password, or SSL/TLS certificate if encryption is enabled.
Authorization: the server checks client permissions, such as which databases can be accessed and which operations are allowed.
Resource allocation: the server allocates memory for session buffers. In PostgreSQL, the server even spawns a new backend worker process dedicated to handling that connection session.
All the steps above take tens to hundreds of milliseconds, especially when the connection travels across networks between servers or performs TLS/SSL negotiation.
Compare that to a simple query execution, which often finishes in just 1 to 5 milliseconds. This means creating a new connection can take tens to hundreds of times longer than running the query itself.
Mental Model
The high cost of creating connections is the main reason why connection pooling exists. Understanding this cost helps you design efficient application architecture.
Here is an example of opening a connection directly in Node.js using the pg library for PostgreSQL:
const { Client } = require("pg");
const client = new Client({
host: "localhost",
user: "app_user",
password: "secret",
database: "mydb",
});
// Open the TCP connection, authenticate, and allocate server resources
await client.connect();
// Execute a query on the active connection
const result = await client.query("SELECT * FROM users");After the query finishes running, the connection must be closed:
// Close the connection session and release server resources
await client.end();Every open connection holds resources on the database server:
If an application opens connections but fails to close them, a connection leak occurs. The application keeps opening new connections until the database server runs out of connection slots, and the server starts rejecting new connections with the error too many clients already.
To keep connections stable in production, pay attention to these three timeouts:
| Timeout | Definition |
|---|---|
| Connection timeout | How long the application will wait when trying to connect before giving up and throwing an error. |
| Idle timeout | How long a connection can sit unused in the pool before it is closed to save resources. |
| Wait timeout | How long the database server will wait for new requests on an idle connection before closing it automatically (MySQL default is 8 hours). |
Trap: connection closed by the server
If the database server closes an idle connection because of wait_timeout, but the application still thinks the connection is open, the next query will fail with errors like connection terminated unexpectedly or broken pipe.
The solution: make sure your application validates connection health before using it, or use a connection pool with automatic health check mechanisms.
Without connection pooling, the lifecycle of a connection for each HTTP request looks like this:
Request 1: Open connection (100 ms) -> Run query (5 ms) -> Close connection (5 ms)
Request 2: Open connection (100 ms) -> Run query (5 ms) -> Close connection (5 ms)
Request 3: Open connection (100 ms) -> Run query (5 ms) -> Close connection (5 ms)Most of the time is wasted opening and closing connections rather than executing queries. This pattern is very inefficient and leads to high application latency.
A connection pool is a cache of database connections that remain open so they can be reused across multiple requests:
How it works:
Here is an example using Pool in Node.js with the pg library:
const { Pool } = require("pg");
// Initialize the pool once when the application boots
const pool = new Pool({
host: "localhost",
user: "app_user",
password: "secret",
database: "mydb",
max: 10, // maximum of 10 simultaneous connections from this application instance
idleTimeoutMillis: 30000, // close idle connections after 30 seconds
connectionTimeoutMillis: 5000, // wait at most 5 seconds for an available connection
});
// pool.query automatically borrows a connection, runs the query, and returns it to the pool
const result = await pool.query("SELECT * FROM users WHERE id = $1", [1]);If you need to run multi-query transactions (BEGIN ... COMMIT), borrow a connection explicitly and make sure it is always returned in a finally block:
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query(
"UPDATE accounts SET balance = balance - 100 WHERE id = $1",
[1],
);
await client.query(
"UPDATE accounts SET balance = balance + 100 WHERE id = $2",
[2],
);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
// Always release the client back to the pool to prevent leaks
client.release();
}| Parameter | Description |
|---|---|
max | Maximum number of connections this pool can open to the database server. |
idleTimeoutMillis | How long an unused connection can stay in the pool before being closed. |
connectionTimeoutMillis | Maximum time a request waits for an available connection before throwing a timeout error. |
maxLifetime | Maximum lifespan of a connection before it is retired and replaced with a fresh one. |
Common mistake: creating a pool inside request handlers
Do not create a new connection pool instance inside a request handler function. A connection pool must be created once when the application starts (as a singleton) and shared across the entire app. Creating a pool per request makes connection problems worse and quickly exhausts server memory.
When sizing connections, engineers often confuse application settings, database configuration, and hardware capacity. These three numbers operate at different levels:
| Level | Parameter | Example Value | Description |
|---|---|---|---|
| Level 1: Application | pool_size | 20 | Configured in application code: maximum connections allowed for one application instance. |
| Level 2: Database Server | max_connections | 100 | Configured on the database server: total limit of connections accepted from all clients. |
| Level 3: Hardware Capacity | core_count × 2 | 8 (on a 4-core server) | Realistic hardware limit: estimated number of queries that the database CPU can run concurrently with high efficiency. |
To understand how these three levels interact, picture a restaurant:
max_connections (100): total seating capacity in the dining room. If all 100 seats are full, new guests are turned away at the door.pool_size (20): number of seats reserved for a specific party or group booking.core_count × 2 (8): number of chefs working in the kitchen who can cook dishes in parallel.A restaurant may hold 100 seated guests. But if the kitchen only has 4 chefs, trying to cook 100 orders at the exact same moment will cause kitchen chaos, exhaust the chefs, and make every dish come out late. Having 100 chairs does not mean you can cook 100 meals at once.
A popular sizing formula was introduced by the team behind HikariCP (a high-performance connection pool in the Java ecosystem):
pool_size ≈ core_count × 2 + effective_spindle_countOn modern servers using SSD storage, there are no spinning hard drive spindles, so effective_spindle_count is close to 0. The practical formula becomes:
pool_size ≈ core_count × 2The most critical point to remember: core_count refers to the CPU cores on the DATABASE SERVER, not your application server.
The database server CPU is the one doing the heavy lifting: parsing SQL, calculating query execution plans, joining tables, sorting rows, and scanning indexes.
The reasoning behind this:
The × 2 multiplier acts as a buffer because active connections do not consume 100% of the CPU at every microsecond.
TIP
Keep in mind that this formula was originally designed for a single monolith application connecting to one database server. When multiple applications or instances connect to the same database, this number represents the total pool budget that must be shared.
Inside the database, queries in progress can be in different states:
| Query State | Uses CPU? | Example Scenario |
|---|---|---|
| Actively executing | Yes | Query is running a sequential table scan or calculating aggregations in memory. |
| Waiting on disk I/O | No | Query is reading pages from disk storage because data is not in cache (buffer pool). |
| Waiting on locks | No | Query is waiting for another transaction to release a lock on a row or table. |
| Idle in transaction | No | Application opened a transaction (BEGIN), but has not sent COMMIT or ROLLBACK yet. |
The risk of "idle in transaction"
Connections stuck in idle in transaction do not burn CPU, but they hold row locks and block background cleanup (such as autovacuum in PostgreSQL). Leaving connections in this state degrades table performance over time.
Because some queries spend time waiting on disk I/O or locks, the core × 2 formula balances CPU utilization and waiting queues. Making the pool much larger than this ratio will not speed up execution once the database CPU reaches capacity.
Many developers assume that increasing the pool size (for example, setting pool_size = 20 on a 4-core database server) will help the application handle traffic faster. This assumption is incorrect.
A larger pool does not make queries run faster. If the database server only has 4 CPU cores, 20 incoming queries can still only be processed by the CPU about 4 at a time. The remaining queries wait in line inside PostgreSQL.
This creates several negative effects:
It is much better for requests to queue in the application than to pile up inside the database server.
When the pool size is bounded proportionally (such as 8 connections), extra requests wait calmly in the application connection pool queue. Queuing inside the application uses lightweight memory and remains controllable. Meanwhile, the database server continues running at peak efficiency without getting overwhelmed. As soon as a query finishes and releases its connection, the next request in line is dispatched to the database.
Let us walk through an architecture scenario commonly seen in production: multiple application services connected to a single database server.
Architecture scenario:
max_connections = 100.First, calculate the total ideal pool capacity for the database server hardware:
Ideal database capacity = 4 CPU cores × 2 = 8 efficient parallel connectionsThis number 8 is the total pool budget for the entire database server, not a quota each application can take for itself. Because 3 services share the same database, divide this ideal capacity across the services:
pool_size per instance ≈ (core_count × 2) / total application instances
pool_size per instance ≈ 8 / 3 ≈ 2 to 3 connections per applicationFor this scenario, allocate the pool sizes as follows:
pool_size = 3pool_size = 3pool_size = 2Key takeaways from this ideal design:
If your application traffic grows and waiting times in the application pool (acquire wait time) become too long, here are your options:
pool_size to 5–6.Sizing principle: measure, do not guess
Use core × 2 as a healthy starting baseline. From there, monitor production metrics: connection acquire wait time, query latency, and active versus idle connection counts. Adjust pool sizes based on real data.
A classic production incident happens when all connection slots are consumed during an emergency:
max_connections = 100.psql) to investigate.To prevent this nightmare scenario, modern databases offer reserved connection slots for administrators:
| Database | Setting / Mechanism | Access Rights |
|---|---|---|
| PostgreSQL | superuser_reserved_connections (default: 3) | Reserved for roles with superuser privileges. |
| MySQL | Provides 1 extra connection slot beyond max_connections | Reserved for users with SUPER or CONNECTION_ADMIN privileges. |
Example calculation in PostgreSQL:
max_connections = 100
superuser_reserved_connections = 3
------------------------------------------------------
Maximum connections for regular applications = 97
Reserved connections for superusers = 3Tasks that must be accounted for in your connection headroom:
pg_stat_activity and terminating stuck queries.ALTER TABLE or deployment scripts.VACUUM.Always ensure the sum of all application pools plus admin headroom stays well below max_connections.
In architectures with hundreds of microservices or serverless platforms (such as AWS Lambda), application instances can scale out dynamically into hundreds of units.
If 100 Lambda functions each open a pool with 5 connections, the database faces 500 simultaneous connections. Because each PostgreSQL connection runs as an independent OS process with dedicated memory, raising max_connections to thousands will waste RAM and risk database instability.
The recommended solution for this architecture is an external connection pooler placed between applications and the database:
Common external poolers used in production:
Here is a summary of best practices for database connection management:
finally block to prevent connection leaks.(core_count × 2) / number of instances of the database server CPU as a starting baseline for application pool size.max_connections.| Parameter | Nature | Role and Purpose |
|---|---|---|
core_count × 2 | Calculation recommendation | Guide for total efficient pool capacity on the database server so CPU query execution runs smoothly. This number is shared across all application instances. |
pool_size | Application configuration | Maximum number of connections allowed for a single application instance. |
max_connections | Database configuration | Hard limit on the total number of physical connections accepted by the database server. |