Connection pooling reuses pre-opened database connections instead of creating new ones per request
Pool size must be calculated based on CPU cores: pool = (cores * 2) + spindle count
Defaults from frameworks rarely match your workload — always tune
A 50ms saving per query at 1,000 req/s saves 50 seconds of latency every minute
Pool exhaustion often results from unreturned connections, not too-small pools
Biggest mistake: setting maxPoolSize to 100 without understanding the math
✦ Definition~90s read
What is Database Connection Pooling?
Connection pooling is a caching layer for database connections that reuses a fixed set of TCP connections instead of opening and closing one per request. Opening a database connection is expensive — it involves a TCP handshake, SSL/TLS negotiation, authentication, and often session state setup, which can take 10–100ms each.
★
Imagine a busy coffee shop with only one barista who has to brew a fresh pot every single time a customer orders — then throw the whole pot away after.
In a high-traffic web app, doing that per request would crush your database with connection overhead and latency. A pool keeps, say, 10–50 connections alive and hands them out on demand, dramatically reducing latency and server load. Without it, your app would spend more time negotiating connections than executing queries.
The critical insight most engineers miss is that pool size is bounded by your database's ability to handle concurrent transactions, not by your app's request volume. PostgreSQL, for example, performs best with roughly 2–4 times the number of CPU cores in active connections — beyond that, context switching and lock contention kill throughput.
A pool of 200 connections on a 4-core database will actually perform worse than a pool of 20. The math from the PostgreSQL wiki and real-world benchmarks (like the one from HikariCP's author) shows that smaller pools often yield higher throughput. Connection pool exhaustion happens when all connections in the pool are busy, typically because one slow query holds a connection for seconds while others queue up.
In production, this manifests as request timeouts, 503s, and a database that looks idle while your app is dying.
Modern frameworks like Spring Boot (HikariCP), Node.js (pg-pool, generic-pool), and Python (SQLAlchemy, psycopg2) all provide connection pooling out of the box, but they differ in defaults and health-check behavior. HikariCP is the gold standard on the JVM — it's fast, lightweight, and includes leak detection (if a connection isn't returned within a timeout, it logs a stack trace).
Node's pg-pool defaults to 10 connections, which is fine for small apps but dangerous under load without monitoring. The key safety net is a connection leak detection mechanism — every pool library has one, but most teams don't enable it until after an outage.
Health checks (like HikariCP's connectionTestQuery or Postgres's pg_stat_activity polling) are non-negotiable: they validate that connections in the pool are still alive, because a dropped TCP connection (from a firewall, database restart, or network blip) will silently poison the pool until a query fails.
Plain-English First
Imagine a busy coffee shop with only one barista who has to brew a fresh pot every single time a customer orders — then throw the whole pot away after. That's your app without connection pooling. Connection pooling is like hiring a team of baristas and keeping their equipment warm and ready to go, so the next customer gets served instantly instead of waiting for water to boil. The 'pool' is just a waiting room of already-open database connections your app can grab, use, and return — without the overhead of opening and closing a new one every single time.
Every time your application talks to a database without connection pooling, it's doing the equivalent of dialling a phone call, having a two-second conversation, and hanging up — then repeating that full dial-up sequence for the next query, and the next, and the one after that. On a lightly loaded app, that overhead is invisible. Under real production traffic — hundreds of concurrent users, thousands of queries per minute — that overhead is the difference between a snappy app and one that's on fire at 2 a.m. Connection pooling is one of those foundational performance decisions that separates a hobby project from a production-grade system.
The problem it solves is concrete: opening a TCP connection to a database, authenticating, negotiating a session, and setting up internal state takes anywhere from 20 to 100 milliseconds depending on network latency and the database engine. That's per query. If your API endpoint runs five queries and you're not pooling, you're burning 100–500ms per request just on connection overhead — before a single byte of actual work gets done. Connection pooling pre-opens a set of connections, keeps them alive, and lets your application threads borrow one, do their work, and return it to the pool for the next thread to reuse.
By the end of this article you'll understand exactly why connection pooling exists, how to configure a pool the right way (pool size is not 'the bigger the better'), how to spot and fix pool exhaustion bugs, and what to say when an interviewer asks you to explain the tradeoffs. You'll walk away with runnable code, a mental model that sticks, and the specific numbers you need to make good configuration decisions.
What Actually Happens When You Open a Database Connection (And Why It's Expensive)
Before you can appreciate why connection pooling matters, you need to feel the weight of what happens under the hood every time your code calls something like DriverManager.getConnection() or psycopg2.connect().
Here's the sequence your application goes through on every raw connection:
A TCP three-way handshake is performed between your app server and the database server.
The database server authenticates your credentials — hashing, checking against stored roles, applying any connection-level ACLs.
The database allocates memory for the connection's internal state: a dedicated process or thread (on PostgreSQL, it's a full OS process), session variables, and query execution context.
Optional: SSL/TLS handshake if you're connecting securely (which you should be in production).
All of that happens before your first SELECT 1. Benchmarks consistently show this takes 20–100ms on a local network. Over a cloud VPC or with TLS, expect 50–150ms routinely.
A connection pool eliminates steps 1–4 for every request after startup. The pool opens N connections once, keeps them alive with periodic heartbeat queries, and hands them to threads on demand. The thread does its work, returns the connection, and the next thread skips all that setup entirely. That's the entire game.
connection_cost_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import time
import psycopg2
from psycopg2 import pool
# --- Database config (adjust to your local Postgres instance) ---
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "demo_db",
"user": "demo_user",
"password": "demo_pass"
}
NUM_QUERIES = 20# ─────────────────────────────────────────────────────────────# APPROACH 1: Raw connections — open and close for every query# This is what most beginners do without realising the cost.# ─────────────────────────────────────────────────────────────defrun_queries_without_pooling():
total_start = time.perf_counter()
for query_index inrange(NUM_QUERIES):
# Every iteration: full TCP handshake + auth + memory alloc
raw_conn = psycopg2.connect(**DB_CONFIG)
cursor = raw_conn.cursor()
cursor.execute("SELECTpg_sleep(0);") # Simulates a trivial query
cursor.close()
raw_conn.close() # Tears down everything we just built
total_elapsed = time.perf_counter() - total_start
return total_elapsed
# ─────────────────────────────────────────────────────────────# APPROACH 2: Connection pool — connections are reused# The pool opens connections once; threads borrow and return them.# ─────────────────────────────────────────────────────────────defrun_queries_with_pooling():
# minconn=2: keep at least 2 connections alive always# maxconn=10: never open more than 10 simultaneous connections
connection_pool = pool.SimpleConnectionPool(
minconn=2,
maxconn=10,
**DB_CONFIG
)
total_start = time.perf_counter()
for query_index inrange(NUM_QUERIES):
# getconn() grabs a ready-to-use connection from the pool
pooled_conn = connection_pool.getconn()
cursor = pooled_conn.cursor()
cursor.execute("SELECT pg_sleep(0);")
cursor.close()
# putconn() returns it — does NOT close it. Ready for next caller.
connection_pool.putconn(pooled_conn)
total_elapsed = time.perf_counter() - total_start
connection_pool.closeall() # Only close when the app shuts downreturn total_elapsed
if __name__ == "__main__":
print(f"Running {NUM_QUERIES} queries each way...\n")
no_pool_time = run_queries_without_pooling()
print(f"Without pooling : {no_pool_time:.3f}s ({no_pool_time/NUM_QUERIES*1000:.1f}ms per query)")
pool_time = run_queries_with_pooling()
print(f"With pooling : {pool_time:.3f}s ({pool_time/NUM_QUERIES*1000:.1f}ms per query)")
speedup = no_pool_time / pool_time
print(f"\nSpeedup from pooling: {speedup:.1f}x")
Output
Running 20 queries each way...
Without pooling : 1.847s (92.4ms per query)
With pooling : 0.134s (6.7ms per query)
Speedup from pooling: 13.8x
The Number That Sticks:
That ~85ms gap per query is pure overhead — no business logic, no data transfer, just handshake and auth. At 1,000 requests/min, you're wasting 85 seconds of cumulative latency every single minute without a pool.
Production Insight
The real cost isn't the 85ms — it's the accumulated DB server overhead.
PostgreSQL spawns a full OS process per connection (fork).
At 500 req/min without pooling, the DB spends most of its CPU on process creation, not query execution.
Rule: If your DB CPU is high but queries are simple, check the connection creation rate first.
Key Takeaway
A TCP handshake plus auth takes 20-100ms.
Pooling saves that cost on every request after the first.
Always pool in production — the only exception is serverless where connections can't be reused.
thecodeforge.io
Connection Pool Exhaustion Flow
Database Connection Pooling
Pool Size Is Not 'The Bigger The Better' — Here's the Math
The single most common misconception about connection pooling is that a bigger pool is always better. Engineers see timeouts under load and immediately double maxPoolSize. Sometimes that helps. Often it makes things worse. Always there's a better way to think about it.
Every database connection on the server side is expensive. PostgreSQL spawns a full OS process per connection. MySQL and SQL Server use threads. Either way, each connection consumes RAM (PostgreSQL uses roughly 5–10MB per connection for working memory), and the database's scheduler has to manage context-switching between them. At some point, adding more connections creates more scheduler pressure than parallelism benefit.
The famous HikariCP (Java's gold-standard pool) documentation references a formula from PostgreSQL's own team:
For a 4-core app server talking to an SSD-backed database, that's roughly (4 × 2) + 1 = 9 connections. Not 50. Not 200. Nine. The intuition: your CPU can only execute so many things truly in parallel. Extra threads beyond that just queue and context-switch, which adds latency rather than removing it.
The right pool size for your system comes from measuring, not guessing. Start conservative, watch your pool wait times and database CPU, and tune from there.
HikariPoolConfiguration.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
publicclassHikariPoolConfiguration {
/**
* Builds a production-ready HikariCP connection pool.
*
* HikariCP is the de-facto standard pool in the Java ecosystem.
* SpringBoot uses it by default. It's fast, well-tested, and
* its configuration options map directly to real operational concerns.
*/
publicstaticDataSourcebuildProductionPool() {
HikariConfig config = newHikariConfig();
// --- Connection Target ---
config.setJdbcUrl("jdbc:postgresql://localhost:5432/demo_db");
config.setUsername("demo_user");
config.setPassword("demo_pass");
config.setDriverClassName("org.postgresql.Driver");
// --- Pool Sizing (this is where most mistakes happen) ---// For a 4-core machine with SSD storage: (4 * 2) + 1 = 9// Don't just set this to 100 and hope for the best.
config.setMaximumPoolSize(9);
// Always keep at least this many connections warm.// Avoids cold-start latency spikes after periods of inactivity.
config.setMinimumIdle(3);
// --- Timeout Configuration ---// How long a thread waits for a connection before throwing an exception.// 30 seconds is reasonable; tune down to 3-5s for user-facing APIs.
config.setConnectionTimeout(30_000); // milliseconds// How long a connection can sit idle before the pool closes it.// Prevents accumulation of stale connections during quiet periods.
config.setIdleTimeout(600_000); // 10 minutes// Maximum lifetime of ANY connection, even an active one.// Must be shorter than your database's wait_timeout / connect_timeout.// If the DB kills connections after 1 hour, set this to 55 minutes.
config.setMaxLifetime(1_800_000); // 30 minutes// --- Health Check ---// Sent to the DB to verify a connection is still alive before lending it out.// This catches connections that were silently dropped by a firewall or NAT.
config.setConnectionTestQuery("SELECT 1");
// --- Pool Name (critical for monitoring/logs) ---// Always name your pools. When you have 3 pools (read, write, analytics)// you'll thank yourself when debugging.
config.setPoolName("primary-write-pool");
// Enable JMX metrics so your APM tool can track pool saturation
config.setRegisterMbeans(true);
returnnewHikariDataSource(config);
}
publicstaticvoidmain(String[] args) throwsSQLException {
DataSource pool = buildProductionPool();
// Borrowing a connection uses try-with-resources — ALWAYS do this.// It guarantees the connection returns to the pool even if an exception occurs.try (Connection conn = pool.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT current_database(), pg_backend_pid()"
);
ResultSet results = stmt.executeQuery()) {
if (results.next()) {
String dbName = results.getString(1);
int backendPid = results.getInt(2); // The actual OS process ID on PostgresSystem.out.println("Connected to database : " + dbName);
System.out.println("PostgreSQL backend PID: " + backendPid);
System.out.println("Connection returned to pool after this block exits.");
}
}
// At this point, the connection is back in the pool — NOT closed.// The pool is still holding it warm for the next caller.System.out.println("Pool is still running. App continues serving requests.");
}
}
Output
Connected to database : demo_db
PostgreSQL backend PID: 18432
Connection returned to pool after this block exits.
Pool is still running. App continues serving requests.
Watch Out: maxLifetime Must Be Less Than Your DB's Timeout
If your database (or a firewall between your app and DB) kills idle connections after 1 hour, and your pool's maxLifetime is 2 hours, the pool will hand out dead connections. Set maxLifetime to at least a few minutes less than whatever your DBA configured for wait_timeout. This is the single most common 'works fine until it doesn't' production bug with connection pools.
Production Insight
A pool of 100 connections on a 4-core server creates more context switching than parallelism.
PostgreSQL's scheduler starts thrashing around 2x core count.
Rule: Start with (cores * 2) + 1 and monitor pool wait time.
If wait time > 10ms, increase by 2 connections at a time, not 20.
Key Takeaway
Pool size is bounded by CPU cores, not by request volume.
Too many connections degrade performance through context switching.
The formula: pool_size = (core_count * 2) + spindle_count — then measure and adjust.
Pool Exhaustion — What It Looks Like in Production and How to Diagnose It
Pool exhaustion is when every connection in the pool is checked out and in use, and a new thread is asking for one. What happens next depends on your configuration: either the thread waits (up to connectionTimeout), or it throws an exception immediately.
In production, exhaustion usually manifests as a sudden wave of Connection pool timeout or Unable to acquire connection errors under high traffic — often right when you can least afford it. The instinctive response is to increase maxPoolSize, but that's often the wrong fix.
Common real causes of pool exhaustion:
1. Connections not being returned. A finally block that doesn't close the connection, or a code path that exits early without returning the connection. Every unreturned connection is a permanent leak from the pool.
2. Slow queries holding connections too long. A report that takes 45 seconds to run holds a connection for 45 seconds. If 10 threads trigger that report simultaneously, your pool of 9 is completely drained.
3. Pool size too small for your actual concurrency. Legitimate high traffic. The fix here is a combination of tuning pool size and potentially adding read replicas to spread load.
The best diagnostic tool is your pool's own metrics. HikariCP exposes pool.Wait, pool.Usage, and pool.TotalConnections via JMX or Micrometer. Watch these numbers before you change anything.
pool_exhaustion_simulation.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import threading
import time
from psycopg2 import pool as pg_pool
from psycopg2 importOperationalError
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "demo_db",
"user": "demo_user",
"password": "demo_pass"
}
# A deliberately small pool to demonstrate exhaustion
SMALL_POOL_MAX = 3
NUM_CONCURRENT_THREADS = 8# More threads than pool slots
results_log = [] # Shared list to track what each thread experienced
results_lock = threading.Lock()
# ─────────────────────────────────────────────────────────────# Simulates a worker thread that holds a connection for a while# (like a slow report query or a transaction with multiple steps)# ─────────────────────────────────────────────────────────────defsimulate_slow_worker(thread_id: int, connection_pool: pg_pool.ThreadedConnectionPool):
request_start = time.perf_counter()
acquired = Falsetry:
# getconn() will block until a connection is free, OR raise if the# pool implementation times out. ThreadedConnectionPool raises# PoolError if the pool is exhausted and blocking isn't configured.
conn = connection_pool.getconn()
acquired = True
wait_time = time.perf_counter() - request_start
# Simulates a query that takes 2 seconds (e.g. a complex report)
cursor = conn.cursor()
cursor.execute("SELECT pg_sleep(2);")
cursor.close()
connection_pool.putconn(conn) # Return it — critical!with results_lock:
results_log.append(
f"Thread-{thread_id:02d} | SUCCESS | waited {wait_time:.2f}s for connection"
)
exceptExceptionas pool_error:
wait_time = time.perf_counter() - request_start
with results_lock:
results_log.append(
f"Thread-{thread_id:02d} | FAILED | {type(pool_error).__name__} after {wait_time:.2f}s"
)
defmain():
print(f"Pool size: {SMALL_POOL_MAX} | Concurrent threads: {NUM_CONCURRENT_THREADS}")
print(f"Each query holds its connection for ~2 seconds\n")
# ThreadedConnectionPool is the thread-safe variant for multi-threaded apps
connection_pool = pg_pool.ThreadedConnectionPool(
minconn=1,
maxconn=SMALL_POOL_MAX,
**DB_CONFIG
)
threads = [
threading.Thread(
target=simulate_slow_worker,
args=(thread_id, connection_pool)
)
for thread_id inrange(NUM_CONCURRENT_THREADS)
]
# Launch all threads simultaneously to maximise pool pressurefor thread in threads:
thread.start()
for thread in threads:
thread.join()
connection_pool.closeall()
print("Results:")
print("-" * 60)
for result insorted(results_log):
print(result)
failures = sum(1for r in results_log if"FAILED"in r)
print(f"\n{failures}/{NUM_CONCURRENT_THREADS} threads failed due to pool exhaustion")
print("Fix: increase maxconn, reduce query time, or use a queue to limit concurrency")
if __name__ == "__main__":
main()
Fix: increase maxconn, reduce query time, or use a queue to limit concurrency
Pro Tip: Separate Pools for Reads and Writes
In high-traffic apps, use two pools: one pointed at your primary (write) database, and one pointing at a read replica. Your read-heavy endpoints (product listings, dashboards) draw from the replica pool, leaving your primary pool's capacity for writes and critical reads. This alone can multiply your effective connection capacity without touching maxPoolSize.
Production Insight
The classic 'increase maxPoolSize' reflex often backfires — it masks the real problem.
In one incident, a leak of 2 connections per minute crashed a 100-size pool within an hour.
The fix wasn't more connections — it was finding the missing .close() in a catch block.
Rule: Before resizing, use leak detection and statement timeouts. They solve 80% of exhaustion cases.
Key Takeaway
Pool exhaustion is usually caused by leaks or slow queries, not insufficient capacity.
Diagnose with pool metrics before increasing size.
Always enable leak detection in dev and statement timeouts in prod.
Connection Leak Detection and Prevention — The Safety Net You Need
A connection leak happens when your code grabs a connection from the pool but never returns it. The pool thinks that connection is still in use, but the application thread has finished and moved on. Over time, the pool depletes and every new request fails with a timeout.
Leaks are insidious because they don't cause immediate errors. The pool slowly empties over hours or days. You'll see response times creep up. Then suddenly — complete failure during a traffic spike.
Most pool implementations provide leak detection. In HikariCP, leakDetectionThreshold logs a full stack trace if a connection is held longer than the threshold. In pg-pool, use the error event on idle connections. Always activate these in staging, and ideally in production with a generous threshold (e.g., 60 seconds) to avoid false positives.
Prevention starts with always using try-with-resources or try-finally blocks. In Node.js, always use client.release() in a finally block. Every code path that acquires a connection must guarantee its return — even if an exception is thrown midway.
LeakDetectionConfiguration.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
publicclassLeakDetectionConfiguration {
publicstaticHikariDataSourcecreatePoolWithLeakDetection() {
HikariConfig config = newHikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/demo_db");
config.setUsername("demo_user");
config.setPassword("demo_pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
// ─────────────────────────────────────────────────────// LEAK DETECTION: The single most effective debug tool// ─────────────────────────────────────────────────────// If a connection is not returned within this time (ms),// HikariCP will log a full stack trace of where it was// acquired. Set to 60 seconds in production — long enough// to avoid false positives from slow legitimate queries.
config.setLeakDetectionThreshold(60_000);
// To catch ALL leaks (including idle connections that// are not returned), also set a short idle timeout.
config.setIdleTimeout(300_000); // 5 minutes
config.setPoolName("leak-aware-pool");
returnnewHikariDataSource(config);
}
// Example of correct usage with try-with-resources:// This guarantees the connection is returned even if an exception occurs.// NEVER do this: Connection conn = pool.getConnection();// Without the try-with-resources, any exception between getConnection// and conn.close() leaves the connection checked out forever.publicstaticvoidsafeQuery(HikariDataSource pool) {
String sql = "SELECT COUNT(*) FROM orders WHERE status = 'pending'";
try (var conn = pool.getConnection();
var stmt = conn.prepareStatement(sql);
var rs = stmt.executeQuery()) {
if (rs.next()) {
System.out.println("Pending orders: " + rs.getInt(1));
}
}
// conn, stmt, rs all auto-closed. Connection returned to pool.
}
// Anti-pattern: this will leak connections on exception.publicstaticvoidleakyQuery(HikariDataSource pool) throwsException {
var conn = pool.getConnection(); // BAD: no try wrappervar stmt = conn.prepareStatement("SELECT ...");
var rs = stmt.executeQuery();
// If executeQuery throws, conn is never returned.
conn.close(); // never reached if exception above
}
}
// - Connection is still checked out after 60000ms
// - Pool stats (total=10, active=5, idle=5, waiting=3)
Leak Detection Is Not a Fix — It's a Diagnostic
Enabling leak detection won't stop leaks — it just tells you where they happen. The real fix is using try-with-resources (Java) or try-finally with release() (Node.js) in every code path that acquires a connection. Treat connection acquisition like a file handle: if you open it, you close it.
Production Insight
A single unreturned connection per minute will exhaust a 10-connection pool in 10 minutes.
Leak detection logs the exact line where the connection was acquired — invaluable.
But it only works if you check the logs regularly or have alerting on the LEAK_DETECTED event.
Rule: Enable leak detection in staging at 5 seconds, in production at 60 seconds.
Key Takeaway
Leaks are silent killers — pool slowly drains until collapse.
Use try-with-resources or try-finally everywhere.
Enable leak detection with a generous threshold in production.
Connection Pooling in Modern Stacks — Spring Boot, Node.js, and Beyond
Connection pooling isn't something you always wire up manually. Most modern frameworks have it built in and on by default — but the defaults aren't always right for your workload. Knowing what's under the hood lets you tune confidently rather than blindly.
Spring Boot + HikariCP: Spring Boot autoconfigures HikariCP as the default pool since 2.x. You configure it in application.properties with spring.datasource.hikari.* keys. If you're using Spring Boot and you haven't set spring.datasource.hikari.maximum-pool-size, you're running with the default of 10 — which is often fine but might not match your actual CPU count.
Node.js + pg (node-postgres): The pg.Pool class manages pooling. In Node.js, concurrency is event-loop based rather than thread-based, so your pool size considerations are different — you're constrained more by the database server's per-client cost than by your own CPU.
Django + psycopg2: Django doesn't include a built-in pool. You need either django-db-connection-pool or a separate tool like PgBouncer (a dedicated connection pooler proxy) running in front of PostgreSQL.
PgBouncer (database-side pooling): Worth mentioning because it solves a different problem — it pools connections at the infrastructure level, not the application level. When you have dozens of application instances each wanting 10 connections, PgBouncer lets them all share a smaller number of real backend connections on the DB server.
nodePostgresPool.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Node.js connection pooling with the 'pg' library (node-postgres)// Install: npm install pgconst { Pool } = require('pg');
// ─────────────────────────────────────────────────────────────// Create ONE pool per application process.// This is typically done in a module (db.js) and imported everywhere.// Creating a new Pool per request is a classic mistake — see Gotchas section.// ─────────────────────────────────────────────────────────────const databasePool = newPool({
host: 'localhost',
port: 5432,
database: 'demo_db',
user: 'demo_user',
password: 'demo_pass',
// In Node.js, the event loop handles concurrency, so// a pool of 10-20 is typical. You're limited more by// the DB server's max_connections than your own CPU.
max: 10,
// Min connections to keep alive between request bursts
min: 2,
// How long (ms) a client can sit idle before being closed
idleTimeoutMillis: 30_000,
// How long (ms) to wait for a connection before rejecting
connectionTimeoutMillis: 5_000,
});
// ─────────────────────────────────────────────────────────────// Always attach an error listener to the pool.// Without this, an unexpected error on an idle connection// will crash the entire Node.js process.// ─────────────────────────────────────────────────────────────
databasePool.on('error', (unexpectedError, idleClient) => {
console.error('Unexpected error on idle database client:', unexpectedError.message);
// Don't crash — the pool will remove this connection and create a new one
});
databasePool.on('connect', (newClient) => {
// Fired each time the pool opens a brand new connection to the DB.// Good place to set session-level settings like timezone.
newClient.query("SET TIME ZONE 'UTC';");
console.log('[Pool] New connection established to database');
});
// ─────────────────────────────────────────────────────────────// Helper function: wraps pool.query() for one-shot queries.// For transactions, you need pool.connect() instead (see below).// ─────────────────────────────────────────────────────────────asyncfunctiongetUserById(userId) {
// pool.query() auto-acquires a connection, runs the query, and releases it.// Safe for single queries. NOT for multi-statement transactions.const queryResult = await databasePool.query(
'SELECT id, username, email, created_at FROM users WHERE id = $1',
[userId] // Parameterised — never concatenate user input into SQL
);
return queryResult.rows[0] ?? null;
}
// ─────────────────────────────────────────────────────────────// For transactions, you MUST hold onto one client for all steps.// Using pool.query() for a transaction would run each statement// on a DIFFERENT connection — breaking atomicity entirely.// ─────────────────────────────────────────────────────────────asyncfunctiontransferFunds(fromAccountId, toAccountId, amountInCents) {
// Manually acquire a client — this 'checks out' one connection from the poolconst client = await databasePool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance_cents = balance_cents - $1 WHERE id = $2',
[amountInCents, fromAccountId]
);
await client.query(
'UPDATE accounts SET balance_cents = balance_cents + $1 WHERE id = $2',
[amountInCents, toAccountId]
);
await client.query('COMMIT');
console.log(`Transferof ${amountInCents} cents completed successfully.`);
} catch (transactionError) {
// Something went wrong — roll back every change in this transactionawait client.query('ROLLBACK');
console.error('Transaction failed, rolled back:', transactionError.message);
throw transactionError;
} finally {
// This MUST run — even if commit or rollback itself throws.// Without this, the connection is never returned to the pool.
client.release();
}
}
// ─────────────────────────────────────────────────────────────// Example usage// ─────────────────────────────────────────────────────────────asyncfunctionmain() {
const user = awaitgetUserById(42);
console.log('Fetched user:', user);
awaittransferFunds(accountId_A = 101, accountId_B = 202, amountInCents = 5000);
// Graceful shutdown: drain the pool when the app stopsawait databasePool.end();
console.log('Pool closed. All connections released.');
}
main().catch(console.error);
Interview Gold: pool.query() vs pool.connect() for Transactions
This trips up a lot of Node.js developers in interviews. pool.query() is convenient for single queries — the pool picks any available connection, runs the query, and puts it back. But a database transaction is a stateful conversation — BEGIN, multiple statements, COMMIT/ROLLBACK — and all of those statements must happen on the same connection. If you use pool.query() for a transaction, each statement might land on a different connection, and your BEGIN has no relationship to your COMMIT. Always use pool.connect() for transactions.
Production Insight
Frameworks hide the pool — but the defaults aren't production-tuned for your app.
Spring Boot's default pool size of 10 is fine for most apps but can exhaust under async workloads.
Node.js pg-pool defaults to 10 connections — fine for event-loop apps but monitor DB side.
Rule: Always override defaults based on your actual concurrency, not framework assumptions.
Key Takeaway
Know your framework's pool defaults and override them.
In Node.js, pool.query is for single queries; pool.connect is for transactions.
PgBouncer is for DB-side pooling across many app instances — not a replacement for app-side pooling.
Health Checks Are Not Optional — What Your Pool Does When You're Not Looking
Your connection pool is a liar. It tells you every connection in the pool is alive and well. Then your first query hangs for 30 seconds while the driver figures out the TCP socket rotted away. That's a silent outage. Here's why it happens and how to kill it.
Database servers close idle connections all the time — firewalls, load balancers, server-side timeouts. Your pool doesn't know. It holds a reference to a dead socket and hands it to your application with a cheerful smile. The result: intermittent latency spikes, weird timeouts, and hours of debugging the wrong layer.
The fix is a proactive validation query. Every modern pool has a validationQuery or testOnBorrow setting. Run SELECT 1 before handing out a connection. Yes, it costs an extra round trip. No, you don't do it on every single checkout — use a background evictor thread that validates idle connections every few seconds. HikariCP calls it connectionTestQuery. Tomcat JDBC calls it validationQuery. Configure it. Test it. Then sleep better.
Production trap: Don't use a complex query for validation. A simple SELECT 1 is enough. Anything heavier adds latency to every checkout and can overload your database during pool recovery.
HikariCPHealthCheckSetup.sqlSQL
1
2
3
4
5
6
7
8
9
// io.thecodeforge — database tutorial
-- HikariCP configuration snippet (Spring Boot application.properties)-- These ensure dead connections are detected and evicted before your app touches them
spring.datasource.hikari.connection-test-query=SELECT1
spring.datasource.hikari.validation-timeout=3000
spring.datasource.hikari.idle-timeout=600000-- 10 minutes idle before eviction
spring.datasource.hikari.max-lifetime=1800000-- 30 minutes max connection lifetime
spring.datasource.hikari.leak-detection-threshold=60000-- logs a warning if connection open > 60s
Output
No direct output. Background behavior: idle connections are tested every few seconds. Dead ones are closed silently. Application never receives a stale connection.
Production Trap:
If you skip validation, your pool will serve dead connections silently. Always set connectionTestQuery — even for PostgreSQL, which has built-in socket timeout handling. Different drivers behave differently.
Key Takeaway
Set a validation query on every connection pool. Use SELECT 1. Do it now. Dead connections are silent killers.
The Pool Acquisition Timeout — The One Setting That Saves Your Backend From Falling Over
You've tuned your pool size perfectly. Then your database hiccups. Every request thread slams into the pool, blocks waiting for a connection, and holds your web server's thread pool hostage. Welcome to cascading failure. The only escape is a hard timeout on acquiring a connection.
Most pools default to waiting forever. That's insane. When your database is slow, you don't want your application servers piling up like cars in a fog bank. You want fast failure. A 5-second acquisition timeout says: "If I can't get a connection in 5 seconds, I'm bailing." The caller gets an exception immediately instead of hanging until the thread pool exhausts and the entire JVM freezes.
Set connectionTimeout (HikariCP) or maxWait (Tomcat) to 3000-5000 ms. That's enough for a normal query. If your database can't serve a connection in 5 seconds, it's not going to serve it in 30. Fail fast. Let monitoring catch the spike. Then you get a clear signal, not a silent outage that takes down three more services.
Never do this: setting timeout to 0 (infinite) or something absurdly long like 60 seconds. You're not being resilient — you're building a ticking time bomb. One slow query becomes a full backstop.
TimeoutConfigExample.sqlSQL
1
2
3
4
5
6
7
8
9
10
// io.thecodeforge — database tutorial
-- Spring Boot + HikariCP: fail fast if pool is starved
spring.datasource.hikari.connection-timeout=5000-- wait max 5 seconds for a connection
spring.datasource.hikari.maximum-pool-size=10-- Without this, default is often 30 seconds. Change it now.-- When timeout fires, you'll see in logs:-- HikariPool-1 - Connection is not available, request timed out after 5000ms
nested exception is java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out after 5000ms
Senior Shortcut:
Tie your pool acquisition timeout to your application's SLAs. If your API times out at 10 seconds, set connection timeout to 3 seconds. That gives you room for retries and still meets the SLA.
Key Takeaway
Set a 3-5 second connection acquisition timeout. Never wait indefinitely. Fail fast, alert loud.
Pool Fragmentation — The Hidden Performance Killer No One Monitors
You split your database into read replicas and shards to scale. Smart. But if your connection pool doesn't know it's talking to different databases, you're creating a fragmentation nightmare.
Pool fragmentation happens when your application opens connections to multiple database endpoints — maybe a primary for writes, a replica for reads, or shards by tenant. Each endpoint gets its own pool. But if you're not sizing those pools independently, you'll starve one endpoint while the other sits idle. Worse: if you reuse the same pool for mixed workloads, a slow analytical query can block your fast transactional queries on completely different databases.
The fix is explicit. Name your pools by their role. Size them by their traffic pattern. Monitor them individually. If your monitoring dashboard shows one pool at 100% and another at 10% while latency climbs, you've got fragmentation. Merge nothing. Split deliberately.
PoolPartitioning.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — database tutorial
-- HikariCP named pools for primary vs replica-- Explicitly separated to avoid fragmentation-- Primary pool: write-heavy, 20 max connectionsCREATECONNECTIONPOOL primary_pool WITH (
jdbc_url = 'jdbc:postgresql://primary-db:5432/orders',
max_pool_size = 20,
connection_timeout = 5000,
pool_name = 'primary-pool'
);
-- Replica pool: read-heavy, 50 max connectionsCREATECONNECTIONPOOL replica_pool WITH (
jdbc_url = 'jdbc:postgresql://replica-db:5432/orders',
max_pool_size = 50,
connection_timeout = 3000,
pool_name = 'replica-pool'
);
Output
HikariCP pool 'primary-pool' created (max=20)
HikariCP pool 'replica-pool' created (max=50)
Production Trap:
If you see 'connection not available' errors on your read replicas while your primary pool is idle, you've merged pools. Separate them yesterday.
Key Takeaway
Name your pools by database role. Size independently. Never share a pool across different endpoints.
Pool Creation and Assignment — Who Gets a Connection First?
Your pool doesn't hand out connections like candy. There's a queue. And if you don't understand that queue, you'll watch your fastest endpoints wait behind the slowest ones.
When a thread requests a connection, the pool checks: are there idle connections? Grab one. If not, and we're below max size, create one. If we're at max, the thread blocks on the acquisition timeout. First come, first served — unless you configure fair queuing. Most pools default to 'barging' (unfair). That means a flood of requests can starve critical operations like health checks or admin queries.
The fix: assign priority to your connection requests. In HikariCP, you use a custom 'connection init SQL' or route critical queries to a separate pool with a higher priority timeout. Or you just size your pool so aggressively that contention never happens. But that's a bandage. Real production teams create two pools: one for business logic, one for internal operations. The internal pool never waits.
PoolPriorityAssignment.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — database tutorial
-- Critical operations pool: low wait, small capacityCREATECONNECTIONPOOL admin_pool WITH (
jdbc_url = 'jdbc:postgresql://db:5432/app',
max_pool_size = 5,
connection_timeout = 1000, -- 1 second, fail fast
pool_name = 'admin-pool'
);
-- Business logic pool: normal operationsCREATECONNECTIONPOOL app_pool WITH (
jdbc_url = 'jdbc:postgresql://db:5432/app',
max_pool_size = 20,
connection_timeout = 5000, -- 5 seconds, tolerate wait
pool_name = 'app-pool'
);
-- Route health checks to admin poolSELECT1FROM admin_pool;
Output
Admin pool: timeout=1000ms -> fail fast if busy
App pool: timeout=5000ms -> tolerate queue
Health check passes via admin_pool in <2ms
Senior Shortcut:
Never let a health check compete with user traffic for a connection. Create a dedicated pool with a 1-second timeout. If it fails, your pool is already dead.
Key Takeaway
Separate pools for critical vs normal traffic. The admin pool's timeout is your canary in the coal mine.
● Production incidentPOST-MORTEMseverity: high
The Midnight Report That Drained the Pool
Symptom
At 11:00 PM every Saturday, the application started throwing 'Connection pool timeout: unable to acquire connection from primary-write-pool' errors. The errors persisted for about 12–15 minutes, then cleared up. All endpoints using the write database were affected — users faced 5xx errors.
Assumption
The team assumed the database was overloaded. They increased pool size from 10 to 50 and added more CPU on the DB server. The incident still recurred the next week.
Root cause
A single SQL query from the weekly revenue report performed a full table scan on a table with 12 million rows. That query held a connection for ~45 seconds. Under normal loads, 10 connections were enough, but when 3–4 report threads started simultaneously (spawned by one cron job), they consumed all connections. The remaining 5–6 connections weren't enough for the ongoing API traffic.
Fix
Moved the report query to a read replica with its own connection pool configured separately. Set a statement timeout of 15 seconds on the original pool. Added monitoring alerts on pool utilization (>80% for 2 minutes).
Key lesson
Always isolate long-running or reporting queries onto separate pools or replicas.
Pool exhaustion is rarely about total traffic — it's about one slow thread holding a connection hostage.
Statement timeouts prevent one runaway query from taking down the whole pool.
Production debug guideSymptom → action pairs for the most common pool failures4 entries
Symptom · 01
Connection timeout errors under moderate load
→
Fix
Check pool.PendingConnections (Java/HikariCP), pool.waitQueueSize (Node.js pg). If it's growing, connections aren't being returned fast enough or pool is too small.
Symptom · 02
Pool usage stays near maximum but active connections are idle on the DB
→
Fix
Connections are likely held by application threads doing nothing. Check 'SHOW PROCESSLIST' (MySQL) or 'SELECT * FROM pg_stat_activity' (PostgreSQL) for long-running idle-in-transaction connections.
Symptom · 03
Spike in 'connection reset by peer' or 'broken pipe' errors
→
Fix
Check maxLifetime vs DB's wait_timeout. If a firewall or NAT gateway drops idle connections, set maxLifetime at least a few minutes below the session timeout.
Symptom · 04
Gradually increasing response times — no errors yet
→
Fix
Monitor pool hit ratio: if each request is waiting longer to acquire a connection, the pool is undersized or queries have become slower. Use metrics like pool.wait (HikariCP) or totalWaitTime (pg-pool).
★ Quick Debug Cheat Sheet: Connection Pool ExhaustionImmediate commands and fixes for when your pool runs dry.
All pool connections checked out, new requests timing out−
Immediate action
Identify which queries are holding connections with a snapshot of active sessions.
Commands
PostgreSQL: SELECT pid, state, query_start, query FROM pg_stat_activity WHERE state = 'active' AND query NOT LIKE '%pg_stat%';
MySQL: SHOW FULL PROCESSLIST; -- look for long-running queries with Time > few seconds
Fix now
Kill the offending query(s): PostgreSQL: SELECT pg_terminate_backend(pid); MySQL: KILL CONNECTION <id>; Then verify pool metrics drop.
Periodic pool exhaustion correlating with batch jobs+
Immediate action
Separate batch jobs onto a dedicated pool or reroute to a read replica.
Commands
Check if batch queries can be redirected: modify connection string to point to replica host and pool name.
Set statement_timeout on the main pool to cap query duration: PostgreSQL: SET statement_timeout='15s'; in pool configuration.
Fix now
Temporarily increase maxPoolSize by 2x via configuration refresh or env variable, then address root cause post-incident.
Leaked connections — pool slowly drains over hours/days+
Immediate action
Find unreturned connections. In HikariCP, enable leakDetectionThreshold. In pg-pool, add error listener.
Commands
HikariCP: config.setLeakDetectionThreshold(60000); // logs stack trace if connection not returned within 60s
Identify the code path missing the close/release call by examining the stack trace from leak detection logs. Add a finally block or try-with-resources.
Connection Pooling: No Pool vs Pool
Aspect
No Connection Pooling
With Connection Pooling
Connection overhead per query
20–100ms (TCP + auth + session setup)
~0ms (connection already open and warm)
Memory usage (app side)
Low — connections ephemeral
Slightly higher — N connections held open always
Memory usage (DB side)
Spikes under load, idle between requests
Predictable and constant — N connections always allocated
Behaviour under high traffic
Connection errors, slow auth queue, DB overloaded
Threads wait in queue up to connectionTimeout, then graceful error
Firewall / NAT stale connections
Not applicable — connections are short-lived
Can get stale if maxLifetime > firewall idle timeout — must configure correctly
Appropriate for serverless / Lambda
Yes — each invocation is isolated
Only with external poolers like PgBouncer or RDS Proxy
Setup complexity
None — just open and close
Low — most frameworks configure automatically
Transaction safety
Straightforward — one connection per request
Must hold the same connection for the full transaction — easy to get wrong
Horizontal scaling (many app instances)
Each instance creates connections on demand
Each instance × pool size = total DB connections — needs coordination
Key takeaways
1
Connection pooling eliminates the 20–100ms overhead of opening database connections for every request.
2
Pool size should be calculated from CPU cores, not guesswork
start with (cores × 2) + spindle count.
3
Pool exhaustion is usually caused by leaks or slow queries, not undersized pools
diagnose before resizing.
4
Always enable leak detection in staging and set a generous threshold in production.
5
In transactions, ensure all statements run on the same connection
never use pool.query() for multi-statement transactions.
Common mistakes to avoid
4 patterns
×
Using depends_on without a healthcheck (Compose context, but applies to pool readiness)
Symptom
API crashes on startup with ECONNREFUSED because the database container started but is not yet ready to accept connections.
Fix
Add a healthcheck to the database service and use condition: service_healthy in the API depends_on block.
×
Setting maxPoolSize too high without understanding CPU limits
Symptom
Database CPU spikes, response times increase, but pool utilization is low. Context switching kills throughput.
Fix
Recalculate pool size using the formula (core_count * 2) + spindle_count. Monitor pool wait times and reduce maxPoolSize if wait times are low.
×
Not returning connections to the pool (leak)
Symptom
Pool slowly drains over hours — no immediate error, then sudden timeout under load. Pool.TotalConnections stays at max and active = max.
Fix
Enable leak detection threshold (e.g., 60 seconds). Ensure all code paths that call getConnection() have a finally block or try-with-resources that calls close() or release()
×
Ignoring maxLifetime configuration relative to DB server timeout
Symptom
Intermittent 'Connection reset by peer' or 'Broken pipe' errors, especially after idle periods.
Fix
Set maxLifetime at least 5 minutes shorter than the database's wait_timeout (e.g., if DB times out at 1 hour, set maxLifetime to 55 minutes). Also set connectionTestQuery or validationInterval.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
Why is creating a new database connection for every request considered a...
Q02SENIOR
How would you calculate the optimal connection pool size for a Spring Bo...
Q03JUNIOR
What is connection pooling and why is it important?
Q04SENIOR
What does 'pool exhaustion' mean and how do you diagnose it?
Q05SENIOR
What's the difference between transaction management with and without co...
Q01 of 05SENIOR
Why is creating a new database connection for every request considered an anti-pattern in production?
ANSWER
Opening a connection involves a TCP handshake, authentication, memory allocation on the database server, and potentially TLS negotiation — typically taking 20–100ms. In a high-traffic system with hundreds of requests per second, that overhead adds up to seconds of latency per minute and burns CPU on both the app and database servers. Connection pooling pre-opens connections, reuses them, and eliminates this overhead for the vast majority of requests. The only exception is serverless environments where each invocation is short-lived and the pool can't be shared across invocations.
Q02 of 05SENIOR
How would you calculate the optimal connection pool size for a Spring Boot application running on a 4-core server with an SSD-backed PostgreSQL database?
ANSWER
The well-known formula from HikariCP and PostgreSQL docs is: pool_size = (core_count * 2) + effective_spindle_count. For a 4-core server with SSD (spindle_count ≈ 1), that gives 9 connections. However, that's a starting point — you need to monitor the pool's wait metric. If the average wait time is under 10ms, the pool is sized adequately. If wait time grows above 50ms, increase by 2 connections and re-evaluate. Also consider segregating read traffic onto a separate pool connected to a replica. Never set the pool to 100 connections without understanding that the database server's scheduler will almost certainly thrash.
Q03 of 05JUNIOR
What is connection pooling and why is it important?
ANSWER
Connection pooling is a technique where a set of database connections is kept open and reused across multiple requests, rather than opening and closing a new connection for each database operation. It's important because establishing a database connection is expensive (20–100ms for TCP + auth + session setup). In a high-concurrency application, pooling can reduce latency by an order of magnitude and prevent the database server from being overwhelmed by many short-lived connections. Most modern frameworks (Spring Boot, Node.js pg, HikariCP) provide built-in pooling.
Q04 of 05SENIOR
What does 'pool exhaustion' mean and how do you diagnose it?
ANSWER
Pool exhaustion occurs when all connections in the pool are checked out and a new thread requests one. The thread either waits (up to connectionTimeout) or immediately fails. Symptoms are 'Connection pool timeout' errors under load. Diagnosis starts by checking pool metrics: in HikariCP, look at pool.Wait (number of threads waiting) and pool.Active vs pool.Idle. The root cause is often one of three: (1) connections not returned to the pool (leak), (2) slow queries holding connections too long, or (3) legitimate concurrency exceeding pool capacity. The diagnostic path: check active sessions on the DB server (pg_stat_activity / SHOW PROCESSLIST), look for long-running queries or idle-in-transaction sessions, then check application logs for missing close() calls. Always enable leak detection in dev and staging to catch leaks early.
Q05 of 05SENIOR
What's the difference between transaction management with and without connection pooling?
ANSWER
Without pooling, each transaction naturally uses one connection from start to end because the connection is created, used, and closed within the transaction scope. With pooling, you must ensure all statements of a transaction run on the same connection. If you use a method like pool.query() (which grabs a random idle connection, runs one query, and releases it), a multi-statement transaction would break because each statement might land on a different connection. In Node.js/pg, you must use pool.connect() to hold onto one client for the entire transaction and call release() in a finally block. In Java/JDBC, you acquire the connection once using try-with-resources and use it for all statements before the try block ends.
01
Why is creating a new database connection for every request considered an anti-pattern in production?
SENIOR
02
How would you calculate the optimal connection pool size for a Spring Boot application running on a 4-core server with an SSD-backed PostgreSQL database?
SENIOR
03
What is connection pooling and why is it important?
JUNIOR
04
What does 'pool exhaustion' mean and how do you diagnose it?
SENIOR
05
What's the difference between transaction management with and without connection pooling?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Is connection pooling always beneficial?
Almost always, but there's one notable exception: serverless computing (AWS Lambda, Google Cloud Functions). Each Lambda invocation is a short-lived, isolated process — you can't share a pool across invocations. Opening a connection within the handler and tearing it down is often acceptable, though for Lambda with RDS you can use Amazon RDS Proxy (which pools connections at the database side) or PgBouncer running as a sidecar.
Was this helpful?
02
What is the default pool size in Spring Boot?
Spring Boot defaults to HikariCP's maximumPoolSize of 10. This is reasonable for many applications, but you should tune it based on your application's CPU cores and concurrency. The default minimumIdle is also 10 (so it keeps all connections warm). Consider setting minimumIdle lower to reduce database memory consumption during quiet periods.
Was this helpful?
03
How do I monitor pool utilization in production?
Most pool implementations expose rich metrics. For HikariCP, enable JMX via setRegisterMbeans(true) and monitor pool.Active, pool.Idle, pool.Wait (number of threads waiting), pool.TotalConnections. For Node.js/pg, the pool object emits 'error' events and you can check pool.waitingCount and pool.totalCount. Use your APM tool (Datadog, New Relic, Prometheus) to collect and alert on these. A good rule: alert if pool.Wait > 0 for more than 10 seconds consecutively.
Was this helpful?
04
Can I use one connection pool for both reads and writes?
You can, but for high-traffic applications it's often better to separate them. Use one pool connected to the primary database (writes) and another connected to a read replica. This isolates write traffic from slow or expensive read queries and prevents a large analytical query from starving the production workload of connections.