Database Connection Pooling: Beyond the Basics

How to size, monitor, and tune connection pools for high-throughput services without exhausting the database.

Baikal Signal
Lower latency, fewer timeouts, and saner database pressure.

Connection pooling is critical for application performance, yet it's one of the most misconfigured components in production systems. This guide explores advanced strategies for optimal pool configuration.

Why Connection Pooling Matters

Establishing a database connection is expensive. It involves TCP handshake, authentication, and session initialization. Connection pooling amortizes this cost by reusing connections.

Performance Impact

Without pooling, each request creates a new connection:

// Anti-pattern: new connection per request
                                                                        app.get('/users', async (req, res) => {
                                                                          const conn = await db.connect(); // 20-50ms overhead
                                                                          const users = await conn.query('SELECT * FROM users');
                                                                          await conn.close();
                                                                          res.json(users);
                                                                        });

With pooling, connections are reused:

// Better: reuse pooled connections
                                                                        const pool = new Pool({ max: 20 });
                                                                        
                                                                        app.get('/users', async (req, res) => {
                                                                          const users = await pool.query('SELECT * FROM users'); // <1ms
                                                                          res.json(users);
                                                                        });

Pool Sizing Strategy

The formula for pool size is not arbitrary. Consider these factors:

The Formula

A good starting point:

pool_size = ((core_count * 2) + effective_spindle_count)

For a 4-core server with SSD storage:

pool_size = (4 * 2) + 1 = 9

Key considerations:

  • More connections ≠ better performance
  • Too many connections cause context switching overhead
  • Database CPU and memory are limiting factors
  • Connection pool size should match your workload pattern

Configuration Parameters

Beyond pool size, these parameters matter:

const pool = new Pool({
                                                                          max: 20,                    // Maximum pool size
                                                                          min: 5,                     // Minimum idle connections
                                                                          idleTimeoutMillis: 30000,   // Close idle connections after 30s
                                                                          connectionTimeoutMillis: 2000, // Fail fast if no connection available
                                                                          maxUses: 7500,              // Retire connections after N uses
                                                                        });

Connection Lifecycle

Understanding the lifecycle helps debug issues:

  1. Connection requested from pool
  2. If available, connection returned immediately
  3. If pool full, request waits (up to timeout)
  4. After use, connection returned to pool
  5. Idle connections closed after timeout

Monitoring Pool Health

Track these metrics in production:

  • Pool utilization: active / total connections
  • Wait time: time requests spend waiting for connections
  • Checkout failures: requests that timeout
  • Connection errors: failed connection attempts
// Export pool metrics
                                                                        pool.on('acquire', () => {
                                                                          metrics.increment('pool.acquire');
                                                                        });
                                                                        
                                                                        pool.on('release', () => {
                                                                          metrics.increment('pool.release');
                                                                        });
                                                                        
                                                                        setInterval(() => {
                                                                          metrics.gauge('pool.size', pool.totalCount);
                                                                          metrics.gauge('pool.idle', pool.idleCount);
                                                                          metrics.gauge('pool.waiting', pool.waitingCount);
                                                                        }, 10000);

Common Pitfalls

1. Connection Leaks

Always return connections to the pool:

// Wrong: connection leak
                                                                        const client = await pool.connect();
                                                                        const result = await client.query('SELECT ...');
                                                                        // client.release() missing!
                                                                        
                                                                        // Correct: always release
                                                                        const client = await pool.connect();
                                                                        try {
                                                                          const result = await client.query('SELECT ...');
                                                                          return result;
                                                                        } finally {
                                                                          client.release(); // Always executes
                                                                        }

2. Pool Per Request

Create the pool once at startup, not per request.

3. Ignoring Database Limits

Your pool size must respect the database's max_connections setting. If you have 10 application instances with pool size 20, you need at least 200 database connections available.

Summary

Connection pooling is not set-and-forget. Right-size your pools based on workload, monitor key metrics, and avoid common pitfalls like connection leaks. Start conservative and adjust based on real production data.