Claude-skill-registry Connection Pooling
Implementing and optimizing database connection pools for high-performance applications.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/connection-pooling" ~/.claude/skills/majiayu000-claude-skill-registry-connection-pooling && rm -rf "$T"
skills/data/connection-pooling/SKILL.mdConnection Pooling
Overview
Connection pooling is a technique used to maintain a cache of database connections that can be reused instead of creating a new connection for each request. This significantly improves application performance by reducing overhead of establishing new connections.
Prerequisites
- Understanding of database connections and TCP/IP networking
- Knowledge of database query execution
- Familiarity with async/await patterns
- Basic understanding of resource management
Key Concepts
What is Connection Pooling and Why It Matters
The Problem Without Pooling
Without connection pooling, each database operation requires:
- TCP Connection Establishment - Network handshake
- Authentication - Verify credentials
- Session Initialization - Set session parameters
- Query Execution - Actual work
- Connection Teardown - Close connection
This process can take 50-500ms, which is significant when multiplied across thousands of requests.
The Solution With Pooling
With connection pooling:
- Borrow Connection - Get from pool (~1ms)
- Query Execution - Actual work
- Return Connection - Back to pool (~1ms)
The pool maintains a set of established connections that are reused across requests.
Benefits
- Performance: 10-100x faster connection acquisition
- Resource Efficiency: Fewer connections to database
- Scalability: Handle more concurrent requests
- Stability: Prevents connection storms
Connection Lifecycle
Pool States
┌─────────────────────────────────────────────────────────────┐ │ Connection Pool │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Idle │ │ Active │ │ Creating │ │ │ │ Connections │◄──►│ Connections │◄──►│ Connections │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ▲ ▲ │ │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
Lifecycle Stages
// 1. Create const pool = new Pool({ host: 'localhost', database: 'mydb', max: 20, // Maximum pool size }); // 2. Acquire (Borrow) const connection = await pool.connect(); // Connection is now marked as active // 3. Use const result = await connection.query('SELECT * FROM users'); // 4. Release (Return) connection.release(); // Connection is now marked as idle // 5. Destroy (if needed) // Pool may destroy connections that are: // - Too old (maxLifetime) // - Idle too long (idleTimeout) // - Failed health check
Implementation Guide
Basic Connection Pool (PostgreSQL)
const { Pool } = require('pg'); const pool = new Pool({ host: 'localhost', database: 'mydb', user: 'user', password: 'pass', // Pool settings max: 20, // Maximum pool size min: 2, // Minimum pool size idleTimeoutMillis: 30000, // Close idle connections after 30s connectionTimeoutMillis: 5000, // Wait 5s for connection // Connection settings application_name: 'myapp', statement_timeout: 30000, }); // Simple query const result = await pool.query('SELECT * FROM users'); // With connection const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('UPDATE users SET name = $1 WHERE id = $2', ['John', 1]); await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } // Event listeners pool.on('connect', (client) => { console.log('New client connected'); }); pool.on('error', (error) => { console.error('Pool error:', error); }); // Graceful shutdown await pool.end();
MySQL Connection Pool
const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'user', password: 'pass', database: 'mydb', // Pool settings waitForConnections: true, connectionLimit: 20, queueLimit: 0, // Connection settings connectTimeout: 10000, acquireTimeout: 10000, timeout: 60000, }); // Simple query const [rows] = await pool.query('SELECT * FROM users'); // With connection const conn = await pool.getConnection(); try { await conn.beginTransaction(); await conn.execute('UPDATE users SET name = ? WHERE id = ?', ['John', 1]); await conn.commit(); } catch (error) { await conn.rollback(); throw error; } finally { conn.release(); } // Event listeners pool.on('acquire', (connection) => { console.log('Connection %d acquired', connection.threadId); }); pool.on('release', (connection) => { console.log('Connection %d released', connection.threadId); }); // Graceful shutdown await pool.end();
Python SQLAlchemy Pool
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import QueuePool # Create engine with pooling engine = create_engine( 'postgresql://user:pass@localhost/mydb', poolclass=QueuePool, pool_size=20, # Number of connections to maintain max_overflow=10, # Additional connections beyond pool_size pool_timeout=30, # Seconds to wait before giving up pool_recycle=3600, # Recycle connections after 1 hour pool_pre_ping=True, # Test connections before using ) # Create session factory Session = sessionmaker(bind=engine) # Usage def get_users(): session = Session() try: users = session.query(User).all() return users finally: session.close() # Context manager from contextlib import contextmanager @contextmanager def session_scope(): session = Session() try: yield session session.commit() except: session.rollback() raise finally: session.close() # Usage with session_scope() as session: user = session.query(User).first() user.name = 'John'
Pool Sizing Strategies
Basic Sizing Formula
A common starting point for pool sizing:
pool_size = (core_count * 2) + effective_spindle_count
For modern SSD-based databases:
pool_size = core_count * 2
Connection Pool vs Database Limits
// Database server configuration max_connections = 100 // PostgreSQL default // Application instances (4 instances) connections_per_instance = 20 // 4 * 20 = 80 total // Leave room for superuser connections, replication, etc.
Dynamic Pool Sizing
class DynamicPool { constructor(options) { this.min = options.min || 2; this.max = options.max || 20; this.connections = []; this.activeConnections = 0; } async getConnection() { // Try to get idle connection const idle = this.connections.find(c => c.state === 'idle'); if (idle) { return idle.acquire(); } // Create new connection if under max if (this.connections.length < this.max) { const conn = await this.createConnection(); this.connections.push(conn); return conn.acquire(); } // Wait for available connection return this.waitForAvailableConnection(); } releaseConnection(conn) { conn.release(); // Destroy excess idle connections this.pruneIdleConnections(); } pruneIdleConnections() { const idle = this.connections.filter(c => c.state === 'idle'); const excess = idle.length - this.min; if (excess > 0) { // Destroy oldest idle connections idle.slice(0, excess).forEach(c => c.destroy()); } } }
Pool Sizing Calculator
function calculatePoolSize(options) { const { cpuCores = 4, dbMaxConnections = 100, appInstances = 1, targetUtilization = 0.75, // 75% utilization } = options; // Calculate connections per instance const totalAvailable = dbMaxConnections * targetUtilization; const connectionsPerInstance = Math.floor(totalAvailable / appInstances); // Use formula: cores * 2, but cap at available const formulaSize = cpuCores * 2; const poolSize = Math.min(formulaSize, connectionsPerInstance); return { poolSize, formulaSize, connectionsPerInstance, totalAvailable, maxConnections: dbMaxConnections, }; } // Example console.log(calculatePoolSize({ cpuCores: 8, dbMaxConnections: 100, appInstances: 4, })); // Output: { poolSize: 16, formulaSize: 16, connectionsPerInstance: 18, ... }
Connection Validation
Test-on-Borrow
Validate connection before giving it to application.
class ValidatingPool { constructor(options) { this.testOnBorrow = options.testOnBorrow !== false; // Default true this.validationQuery = options.validationQuery || 'SELECT 1'; } async getConnection() { const conn = await this.acquireConnection(); if (this.testOnBorrow) { try { await conn.query(this.validationQuery); } catch (error) { // Connection is bad, destroy and get another await conn.destroy(); return this.getConnection(); } } return conn; } }
Test-on-Return
Validate connection before returning to pool.
class ValidatingPool { constructor(options) { this.testOnReturn = options.testOnReturn || false; } async releaseConnection(conn) { if (this.testOnReturn) { try { await conn.query('SELECT 1'); } catch (error) { // Connection is bad, destroy it await conn.destroy(); return; } } conn.release(); } }
Test-While-Idle
Periodically validate idle connections.
class IdleValidatingPool { constructor(options) { this.idleValidationInterval = options.idleValidationInterval || 60000; // 1 minute this.startIdleValidation(); } startIdleValidation() { setInterval(() => { this.validateIdleConnections(); }, this.idleValidationInterval); } async validateIdleConnections() { const idleConnections = this.connections.filter(c => c.state === 'idle' && Date.now() - c.lastValidated > this.idleValidationInterval ); for (const conn of idleConnections) { try { await conn.query('SELECT 1'); conn.lastValidated = Date.now(); } catch (error) { await conn.destroy(); } } } }
Timeout Configurations
Connection Timeout
Time to wait for a connection from pool.
const pool = new Pool({ host: 'localhost', connectionTimeoutMillis: 5000, // 5 seconds }); try { const conn = await pool.connect(); // ... } catch (error) { if (error.code === 'CONNECTION_TIMEOUT') { console.error('Timeout waiting for connection'); } }
Idle Timeout
Time after which idle connections are closed.
const pool = new Pool({ host: 'localhost', idleTimeoutMillis: 30000, // 30 seconds // Connections idle for >30s will be closed });
Max Lifetime
Maximum time a connection can exist before being closed.
const pool = new Pool({ host: 'localhost', maxLifetimeMillis: 3600000, // 1 hour // Connections older than 1 hour will be closed });
Query Timeout
Time limit for individual queries.
const pool = new Pool({ host: 'localhost', query_timeout: 30000, // 30 seconds }); try { await pool.query('SELECT * FROM large_table'); } catch (error) { if (error.code === 'QUERY_TIMEOUT') { console.error('Query timed out'); } }
Complete Timeout Configuration
const pool = new Pool({ host: 'localhost', database: 'mydb', user: 'user', password: 'pass', // Pool timeouts connectionTimeoutMillis: 5000, // Wait for connection idleTimeoutMillis: 30000, // Close idle connections maxLifetimeMillis: 3600000, // Close old connections // Query timeout query_timeout: 30000, // Statement timeout (PostgreSQL) statement_timeout: '30s', });
Connection Leaks Detection and Prevention
What is a Connection Leak?
A connection leak occurs when a connection is acquired from pool but never returned, causing pool to eventually run out of available connections.
Detection
class LeakDetectingPool { constructor(options) { this.leakDetectionThreshold = options.leakDetectionThreshold || 30000; // 30s this.borrowedConnections = new Map(); } async getConnection() { const conn = await this.acquireConnection(); const borrowId = generateId(); this.borrowedConnections.set(borrowId, { connection: conn, borrowedAt: Date.now(), stackTrace: new Error().stack, }); // Set timeout to detect leak setTimeout(() => { const borrowed = this.borrowedConnections.get(borrowId); if (borrowed) { console.error('Potential connection leak detected!'); console.error('Connection borrowed at:', borrowed.borrowedAt); console.error('Stack trace:', borrowed.stackTrace); } }, this.leakDetectionThreshold); return { connection: conn, release: () => this.releaseConnection(borrowId), }; } releaseConnection(borrowId) { const borrowed = this.borrowedConnections.get(borrowId); if (!borrowed) { console.warn('Connection already released or never borrowed'); return; } borrowed.connection.release(); this.borrowedConnections.delete(borrowId); } } // Usage const { connection, release } = await pool.getConnection(); try { await connection.query('SELECT * FROM users'); } finally { release(); // Always release! }
Prevention with Automatic Cleanup
class AutoCleaningPool { constructor(options) { this.autoCleanupInterval = options.autoCleanupInterval || 60000; this.borrowedConnections = new Map(); this.startAutoCleanup(); } startAutoCleanup() { setInterval(() => { this.cleanupStaleConnections(); }, this.autoCleanupInterval); } cleanupStaleConnections() { const now = Date.now(); for (const [borrowId, borrowed] of this.borrowedConnections) { const age = now - borrowed.borrowedAt; if (age > this.leakDetectionThreshold) { console.warn(`Force returning leaked connection (age: ${age}ms)`); borrowed.connection.release(); this.borrowedConnections.delete(borrowId); } } } }
Using with try-finally Pattern
// Always use try-finally to ensure release async function getUsers() { const { connection, release } = await pool.getConnection(); try { return await connection.query('SELECT * FROM users'); } finally { release(); } } // Or with async resource tracking async function withConnection(fn) { const { connection, release } = await pool.getConnection(); try { return await fn(connection); } finally { release(); } } // Usage const users = await withConnection(async (conn) => { return await conn.query('SELECT * FROM users'); });
Pool Monitoring and Metrics
Basic Metrics Collection
class MonitoredPool { constructor(options) { this.metrics = { totalRequests: 0, totalWaitTime: 0, totalQueryTime: 0, errors: 0, timeouts: 0, }; } async getConnection() { const startTime = Date.now(); this.metrics.totalRequests++; try { const conn = await this.acquireConnection(); const waitTime = Date.now() - startTime; this.metrics.totalWaitTime += waitTime; return { connection: conn, query: async (sql, params) => { const queryStart = Date.now(); try { const result = await conn.query(sql, params); const queryTime = Date.now() - queryStart; this.metrics.totalQueryTime += queryTime; return result; } catch (error) { this.metrics.errors++; throw error; } }, release: () => conn.release(), }; } catch (error) { if (error.code === 'CONNECTION_TIMEOUT') { this.metrics.timeouts++; } this.metrics.errors++; throw error; } } getMetrics() { const avgWaitTime = this.metrics.totalRequests > 0 ? this.metrics.totalWaitTime / this.metrics.totalRequests : 0; const avgQueryTime = this.metrics.totalRequests > 0 ? this.metrics.totalQueryTime / this.metrics.totalRequests : 0; return { ...this.metrics, avgWaitTime, avgQueryTime, errorRate: this.metrics.totalRequests > 0 ? this.metrics.errors / this.metrics.totalRequests : 0, }; } }
Real-time Pool Status
function getPoolStatus(pool) { return { totalCount: pool.totalCount, idleCount: pool.idleCount, waitingCount: pool.waitingCount, maxCount: pool.options.max, minCount: pool.options.min, utilization: pool.totalCount / pool.options.max, }; } // Monitor periodically setInterval(() => { const status = getPoolStatus(pool); console.log('Pool Status:', status); // Alert if pool is nearly exhausted if (status.utilization > 0.9) { console.warn('Pool utilization high:', status.utilization); } }, 5000);
Prometheus Metrics
const promClient = require('prom-client'); // Create metrics const poolSizeGauge = new promClient.Gauge({ name: 'db_pool_size', help: 'Current pool size', labelNames: ['database'], }); const poolIdleGauge = new promClient.Gauge({ name: 'db_pool_idle', help: 'Number of idle connections', labelNames: ['database'], }); const poolWaitingGauge = new promClient.Gauge({ name: 'db_pool_waiting', help: 'Number of clients waiting for connection', labelNames: ['database'], }); const poolQueryDuration = new promClient.Histogram({ name: 'db_query_duration_seconds', help: 'Query execution time', labelNames: ['database', 'operation'], buckets: [0.001, 0.01, 0.1, 1, 10], }); // Update metrics periodically setInterval(() => { const status = getPoolStatus(pool); poolSizeGauge.set({ database: 'mydb' }, status.totalCount); poolIdleGauge.set({ database: 'mydb' }, status.idleCount); poolWaitingGauge.set({ database: 'mydb' }, status.waitingCount); }, 5000); // Track query duration async function queryWithMetrics(sql) { const end = poolQueryDuration.startTimer({ database: 'mydb', operation: 'select' }); try { return await pool.query(sql); } finally { end(); } }
PostgreSQL Connection Poolers
PgBouncer
PgBouncer is a lightweight connection pooler for PostgreSQL.
Installation:
# Ubuntu/Debian sudo apt-get install pgbouncer # macOS brew install pgbouncer # From source wget https://pgbouncer.github.io/downloads/files/1.18.0/pgbouncer-1.18.0.tar.gz tar xzf pgbouncer-1.18.0.tar.gz cd pgbouncer-1.18.0 ./configure && make && sudo make install
Configuration (
):pgbouncer.ini
[databases] mydb = host=localhost port=5432 dbname=mydb [pgbouncer] pool_mode = transaction listen_addr = 127.0.0.1 listen_port = 6432 auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt max_client_conn = 1000 default_pool_size = 25 reserve_pool_size = 5 reserve_pool_timeout = 3 server_lifetime = 3600 server_idle_timeout = 600
User list (
):userlist.txt
"username" "md5hash"
Generate MD5 hash:
echo -n "usernamepassword" | md5sum
Pool Modes:
- Session Pooling: One server connection per client connection
- Transaction Pooling: Server connection returned after each transaction (recommended)
- Statement Pooling: Server connection returned after each statement
Starting PgBouncer:
pgbouncer -d /etc/pgbouncer/pgbouncer.ini
Pgpool-II
Pgpool-II is a more feature-rich connection pooler with additional capabilities.
Installation:
# Ubuntu/Debian sudo apt-get install pgpool2 # macOS brew install pgpool2
Configuration (
):pgpool.conf
# Connection settings listen_addresses = '*' port = 9999 # Pooling connection_cache = on num_init_children = 32 max_pool = 4 child_life_time = 300 connection_life_time = 0 # Load balancing load_balance_mode = on backend_hostname0 = 'db1.example.com' backend_port0 = 5432 backend_weight0 = 1 backend_hostname1 = 'db2.example.com' backend_port1 = 5432 backend_weight1 = 1
Serverless Considerations
Cold Start Impact
Serverless functions start cold and need to establish new connections each time.
// Bad: New connection each invocation exports.handler = async (event) => { const pool = new Pool({ /* ... */ }); const result = await pool.query('SELECT * FROM users'); return result; };
Connection Reuse
// Better: Reuse connection across invocations let pool; async function getPool() { if (!pool) { pool = new Pool({ host: process.env.DB_HOST, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 5, // Lower max for serverless idleTimeoutMillis: 10000, // Shorter idle timeout }); } return pool; } exports.handler = async (event) => { const pool = await getPool(); const result = await pool.query('SELECT * FROM users'); return result; };
AWS Lambda RDS Proxy
Use AWS RDS Proxy for Lambda functions:
// Connect through RDS Proxy const pool = new Pool({ host: process.env.RDS_PROXY_ENDPOINT, // Proxy endpoint database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 5, });
Connection Limits
Serverless platforms have connection limits:
// Calculate pool size based on concurrency const maxConcurrentExecutions = 1000; // Lambda limit const avgDuration = 100; // 100ms per request const connectionsPerSecond = maxConcurrentExecutions / (avgDuration / 1000); const poolSize = Math.min(connectionsPerSecond, 20); // Cap at 20
Pool Per Tenant in Multi-Tenant Apps
Tenant-Specific Pools
class TenantPoolManager { constructor() { this.pools = new Map(); // tenantId -> pool } async getPool(tenantId) { if (!this.pools.has(tenantId)) { const config = await this.getTenantConfig(tenantId); const pool = new Pool({ host: config.host, database: config.database, user: config.user, password: config.password, max: 10, // Smaller pools per tenant }); this.pools.set(tenantId, pool); } return this.pools.get(tenantId); } async closePool(tenantId) { const pool = this.pools.get(tenantId); if (pool) { await pool.end(); this.pools.delete(tenantId); } } async closeAll() { for (const [tenantId, pool] of this.pools) { await pool.end(); } this.pools.clear(); } } // Usage const poolManager = new TenantPoolManager(); async function tenantQuery(tenantId, query) { const pool = await poolManager.getPool(tenantId); return await pool.query(query); }
Schema-Based Multi-Tenancy
// Single pool, multiple schemas const pool = new Pool({ host: 'localhost', database: 'mydb', user: 'user', password: 'pass', }); async function tenantQuery(tenantId, query) { const client = await pool.connect(); try { // Set search path to tenant schema await client.query(`SET search_path TO tenant_${tenantId}`); return await client.query(query); } finally { client.release(); } }
Troubleshooting Pool Exhaustion
Symptoms
- Application hangs waiting for connections
- "Connection timeout" errors
- Slow response times
Diagnosis
// Check pool status function diagnosePool(pool) { const status = { totalCount: pool.totalCount, idleCount: pool.idleCount, waitingCount: pool.waitingCount, maxCount: pool.options.max, utilization: pool.totalCount / pool.options.max, }; console.log('Pool Status:', status); if (status.waitingCount > 10) { console.warn('Many clients waiting for connections'); } if (status.utilization > 0.9) { console.warn('Pool nearly exhausted'); } return status; }
Common Causes
-
Connection Leaks
// Bad: Connection not released const conn = await pool.connect(); await conn.query('SELECT * FROM users'); // Forgot: conn.release() // Good: Always release const conn = await pool.connect(); try { await conn.query('SELECT * FROM users'); } finally { conn.release(); } -
Long-Running Queries
// Bad: Long query holds connection const conn = await pool.connect(); await conn.query('SELECT * FROM huge_table'); // Takes minutes // Good: Use cursor or pagination const conn = await pool.connect(); const cursor = conn.query(new Cursor('SELECT * FROM huge_table')); while (true) { const rows = await cursor.read(1000); if (rows.length === 0) break; // Process rows } -
Pool Too Small
// Increase pool size const pool = new Pool({ max: 50, // Increase from 20 }); -
Database Connection Limit Reached
-- Check current connections SELECT count(*) FROM pg_stat_activity; -- Check max connections SHOW max_connections; -- Increase if needed ALTER SYSTEM SET max_connections = 200;
Best Practices
-
Pool Sizing
- Start with
cpu_cores * 2 - Monitor and adjust based on metrics
- Consider database connection limits
- Account for multiple application instances
- Start with
-
Timeout Configuration
- Set connection timeout (5-10s)
- Set query timeout (30-60s)
- Set idle timeout (30-60s)
- Set max lifetime (1-8 hours)
-
Connection Validation
- Enable test-on-borrow in development
- Use test-while-idle in production
- Set appropriate validation interval
-
Error Handling
- Always release connections in finally blocks
- Handle timeout errors gracefully
- Log connection errors for debugging
-
Monitoring
- Track pool utilization
- Monitor wait times
- Alert on pool exhaustion
- Track connection errors
Common Mistakes
-
Not Releasing Connections
// Bad const conn = await pool.connect(); await conn.query('SELECT * FROM users'); // Connection leaked! // Good const conn = await pool.connect(); try { await conn.query('SELECT * FROM users'); } finally { conn.release(); } -
Pool Too Large
// Bad: Too many connections const pool = new Pool({ max: 1000 }); // Overkill // Good: Appropriate size const pool = new Pool({ max: 20 }); -
No Connection Validation
// Bad: No validation const pool = new Pool({}); // Good: Enable validation const pool = new Pool({ idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }); -
Long Transactions
// Bad: Long transaction holds connection const conn = await pool.connect(); await conn.query('BEGIN'); // ... lots of processing ... await conn.query('COMMIT'); // Good: Keep transactions short const conn = await pool.connect(); try { await conn.query('BEGIN'); await conn.query('UPDATE users SET name = $1', ['John']); await conn.query('COMMIT'); } catch (error) { await conn.query('ROLLBACK'); throw error; } finally { conn.release(); }