Database Connection Pooling
Configure and tune database connection pools to maximize throughput while preventing connection exhaustion.
Overview
Connection pooling reuses established database connections instead of creating a new one per request. Each new connection requires a TCP handshake, TLS negotiation, and authentication, adding 20-100ms of overhead. Under load, creating connections per request exhausts the database’s connection limit and causes cascading failures.
When to Use
Use this resource when:
- Your application opens too many connections and the database rejects new requests
- Latency spikes occur because establishing a TCP + TLS + auth handshake on every request is expensive
- You need to tune connection limits for serverless or high-concurrency architectures
Solution
Python (psycopg2 + psycopg2.pool)
from psycopg2 import pool
# Create a connection pool with min and max connections
pg_pool = pool.ThreadedConnectionPool(
minconn=5,
maxconn=20,
host='localhost',
port=5432,
dbname='myapp',
user='postgres',
password='secret'
)
def query_db(sql, params=None):
conn = pg_pool.getconn()
try:
with conn.cursor() as cur:
cur.execute(sql, params or ())
return cur.fetchall()
finally:
pg_pool.putconn(conn)
# Always return connections to the pool
results = query_db("SELECT * FROM users WHERE active = %s", (True,))
JavaScript (pg Pool)
import { Pool } from 'pg';
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'postgres',
password: 'secret',
max: 20, // max connections
min: 5, // min connections kept ready
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
async function queryDb(sql, params) {
const client = await pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
const users = await queryDb('SELECT * FROM users WHERE active = $1', [true]);
Java (HikariCP)
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/myapp");
config.setUsername("postgres");
config.setPassword("secret");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setIdleTimeout(30000);
config.setConnectionTimeout(2000);
config.setMaxLifetime(1800000);
HikariDataSource ds = new HikariDataSource(config);
try (Connection conn = ds.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE active = ?")) {
stmt.setBoolean(1, true);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
}
Explanation
A connection pool maintains a set of open database connections. When a request needs a connection, it borrows one from the pool, uses it, and returns it. This eliminates the per-request connection overhead.
Pool sizing is the most critical tuning parameter. Too few connections cause requests to queue. Too many connections overwhelm the database. A common formula: pool_size = (core_count * 2) + effective_spindle_count.
Idle timeout closes connections that haven’t been used for a while, freeing database resources during low traffic. Max lifetime prevents long-lived connections from accumulating stale state or hitting database-side timeouts.
Variants
| Pool Library | Language | Key Feature |
|---|---|---|
| psycopg2.pool | Python | ThreadedConnectionPool for multi-threaded apps |
| pg Pool | Node.js | Built-in promise support, auto-reconnect |
| HikariCP | Java | Fastest JDBC pool, metrics via Micrometer |
| PgBouncer | External | Server-side pooler, multiplexes connections |
What Works
- Size pools based on database capacity, not application thread count
- Set idle timeout to close unused connections during low traffic periods
- Monitor pool metrics: active, idle, and waiting connections
- Use a server-side pooler like PgBouncer for serverless or many small clients
- Always return connections in a finally block to prevent leaks
Common Mistakes
- Setting max pool size too high, overwhelming the database with connections
- Not returning connections to the pool, causing connection leaks
- Using the same pool for transactional and read-only queries
- Not monitoring wait times, letting slow queries block the entire pool
- Forgetting to set max lifetime, causing stale connections after database restarts
Additional Common Mistakes
-
Not configuring
connectionTimeout. Without a timeout, requests block indefinitely when the pool is exhausted. Set 2-5 seconds. -
Sharing a single pool across async and sync code. Mixing async frameworks (asyncio, Node.js) with sync pool libraries causes deadlocks. Use async-compatible pools.
-
Creating multiple pool instances. Each pool opens its own connections. Multiple pools in one process multiply the connection count and can exceed
max_connections. -
Not draining pools on shutdown. Failing to close pools on application exit leaves orphaned connections on the database server.
-
Using
pool_mode = sessionin PgBouncer for serverless. Serverless functions open and close connections rapidly. Usetransactionmode to multiplex.
Performance Tips
- Monitor
pg_stat_activityfor connection counts. Track how many connections each application uses:
SELECT application_name, state, COUNT(*)
FROM pg_stat_activity
GROUP BY application_name, state
ORDER BY count DESC;
- Use
pg_stat_statementsto find queries holding connections. Long-running queries occupy pool connections. Identify them:
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
- Benchmark pool sizes with load testing. Use
pgbenchorwrkto find the optimal pool size for your workload:
pgbench -c 20 -j 4 -T 60 -h localhost -p 5432 mydb
-
Use
LISTEN/NOTIFYwith a dedicated connection. PostgreSQLLISTENholds a connection. Use a separate pool or a single dedicated connection for event listeners. -
Tune
work_memper connection. Each connection allocateswork_memfor sorts and hashes. With 20 connections andwork_mem = 64MB, that’s 1.28GB just for sort memory.
Frequently Asked Questions
How many connections should my pool have?
Start with ((core_count * 2) + disk_spindles) and tune from there. For PostgreSQL, the default max_connections is 100. If multiple services connect, divide that budget across them. PgBouncer can multiplex thousands of clients onto a small pool.
Should I use PgBouncer instead of application-level pooling?
Use both. PgBouncer multiplexes application connections to a smaller set of database connections, which helps with serverless and many small services. Application-level pooling reduces connection latency and gives you per-request metrics.
How do I detect connection leaks?
Monitor the pool's active count. If it steadily increases and never drops, connections are not being returned. In Java, HikariCP logs leaks after leakDetectionThreshold (default 0, set to 60000ms). In Node.js, track pool.totalCount vs pool.idleCount.
SQLAlchemy Connection Pool (Python)
from sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:pass@localhost/mydb",
pool_size=10,
max_overflow=20,
pool_timeout=30,
pool_recycle=1800,
pool_pre_ping=True
)
with engine.connect() as conn:
result = conn.execute("SELECT 1")
print(result.scalar())
pool_size: base number of connectionsmax_overflow: additional connections allowed beyondpool_sizepool_timeout: seconds to wait for a connection before raising an errorpool_recycle: seconds before a connection is recycled (prevents stale connections)pool_pre_ping: tests connection validity before use (adds slight overhead)
PgBouncer Server-Side Pooling
; pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
server_lifetime = 3600
# Start PgBouncer
pgbouncer -d /etc/pgbouncer/pgbouncer.ini
# Connect through PgBouncer (port 6432 instead of 5432)
psql -h localhost -p 6432 -U postgres myapp
Pool modes:
session: one server connection per client session (default)transaction: server connection assigned per transaction (recommended for most apps)statement: server connection assigned per statement (no multi-statement transactions)
Monitoring Pool Health
# Python: monitor psycopg2 pool
print(f"Current connections: {pg_pool._used}")
print(f"Available: {pg_pool._pool}")
// Node.js: monitor pg Pool
console.log({
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount
});
// Java: monitor HikariCP
HikariPoolMXBean poolProxy = ds.getHikariPoolMXBean();
System.out.println("Active: " + poolProxy.getActiveConnections());
System.out.println("Idle: " + poolProxy.getIdleConnections());
System.out.println("Waiting: " + poolProxy.getThreadsAwaitingConnection());
Pool Sizing Formula
pool_size = (core_count * 2) + effective_spindle_count
For SSD-only setups with no spinning disks:
pool_size = core_count * 2
For PostgreSQL with max_connections = 100 and 4 services:
per_service_pool = 100 / 4 = 25 connections
Related Resources
PostgreSQL Query Optimization and Indexing Strategies
Analyze and optimize slow PostgreSQL queries using EXPLAIN, proper indexing, partial indexes, and query rewriting to reduce execution time from seconds to milliseconds
RecipeDatabase Transactions
How to use ACID transactions to ensure data integrity across Python, JavaScript, and Java with SQL examples.
GuideDatabase Normalization — 1NF to 5NF Explained
A visual guide to database normalization: learn 1NF through 5NF with practical examples, when to apply each form, and how to balance normalization with performance.
RecipeDatabase Replication
Set up and manage database replication for high availability, read scaling, and disaster recovery with primary-replica architectures.
RecipeDatabase Schema Evolution
Evolve database schemas safely with backward-compatible changes, versioned migrations, and online DDL operations in production environments.
RecipeUUID Generation in Python, JavaScript, and Java
Generate universally unique identifiers (UUIDs) for database keys, session tokens, and resource naming across Python, JavaScript, and Java.