How to Configure Connection Pool Settings for Node.js in Production
I deployed our Node.js microservices to production with 20 replicas, and everything seemed fine during testing. Then a traffic spike hit, and suddenly the entire system ground to a halt. The logs showed a familiar error:
Error: sorry, too many clients alreadyPostgreSQL was rejecting connections. But we had configured everything correctly, or so I thought. The problem wasn’t our database settings—it was our connection pool math.
The Problem: Default Pool Settings Don’t Scale
When I investigated, I found the culprit. We were using the default connection pool size for pg (node-postgres), which is 10 connections per instance. With 20 replicas running, that meant:
- 20 replicas x 10 connections = 200 database connections
- PostgreSQL max_connections default: 100
We were trying to open twice as many connections as the database allowed. The math was simple, but I had completely overlooked it.
This isn’t just a PostgreSQL problem. Redis, HTTP clients—anything with a connection pool has defaults that work fine for development but fall apart in production with multiple instances.
Understanding Connection Pool Sizing
The key insight is that each Node.js instance maintains its own connection pool. If you have:
- N replicas running your Node.js application
- M max_connections on your database
- Each replica opening P pool connections
Then you must ensure: N x P < M
Let me walk through how I fixed this.
Step 1: Calculate the Right Pool Size
The formula is straightforward:
pool_size_per_instance = floor(max_connections / number_of_instances) - safety_bufferThe safety buffer reserves connections for admin tasks, migrations, and emergency access.
With 20 replicas and PostgreSQL max_connections = 200:
pool_size = floor(200 / 20) - 5 = 5 connections per instanceStep 2: Configure PostgreSQL Pool with Calculated Values
I created a configuration module that calculates pool size based on environment variables:
import { Pool } from 'pg'
// Calculate based on your infrastructureconst REPLICA_COUNT = parseInt(process.env.REPLICA_COUNT || '1', 10)const MAX_DB_CONNECTIONS = parseInt(process.env.MAX_DB_CONNECTIONS || '100', 10)const SAFETY_BUFFER = 5 // Reserve connections for admin/maintenance
const poolSizePerReplica = Math.floor(MAX_DB_CONNECTIONS / REPLICA_COUNT) - SAFETY_BUFFER
const pool = new Pool({ // Connection details from environment variables host: process.env.PGHOST || 'localhost', port: parseInt(process.env.PGPORT || '5432', 10), database: process.env.PGDATABASE, user: process.env.PGUSER, password: process.env.PGPASSWORD,
// Pool sizing - THE CRITICAL SETTINGS max: Math.max(poolSizePerReplica, 2), // Minimum 2 connections per instance min: Math.max(Math.floor(poolSizePerReplica * 0.3), 1), // Keep 30% of max alive
// Timeouts - Production-appropriate values idleTimeoutMillis: 30000, // Close idle connections after 30s connectionTimeoutMillis: 5000, // Fail fast if can't connect (5s) maxLifetimeSeconds: 1800, // Rotate connections every 30 minutes
// SSL for production ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : false})
// CRITICAL: Handle pool errors to prevent crashespool.on('error', (err, client) => { console.error('Unexpected error on idle PostgreSQL client', err) // Consider alerting/monitoring integration here})
// Monitoring helperexport function getPoolStats() { return { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, maxConfigured: pool.options.max }}
export default poolWhy These Settings Matter
- max: The upper limit of connections in the pool. This is the critical setting.
- min: Maintains a baseline of open connections for faster response times.
- idleTimeoutMillis: Closes connections that haven’t been used, freeing resources.
- connectionTimeoutMillis: Fails fast rather than hanging when the database is unreachable.
- maxLifetimeSeconds: Rotates connections to prevent long-lived connection issues.
Step 3: Set Environment Variables
The configuration reads from environment variables, making it easy to adjust per environment:
PGHOST=your-postgres-hostPGPORT=5432PGDATABASE=production_dbPGUSER=app_userPGPASSWORD=your-secure-password
# Infrastructure awareness - CRITICALREPLICA_COUNT=20MAX_DB_CONNECTIONS=200
# EnvironmentNODE_ENV=productionThe REPLICA_COUNT and MAX_DB_CONNECTIONS variables are essential. Without them, you can’t calculate the correct pool size.
Step 4: Configure Redis for Production
Redis handles connections differently than PostgreSQL, but similar principles apply. I use ioredis with explicit configuration:
import Redis from 'ioredis'
// Redis connection configuration for productionconst redisConfig = { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10), password: process.env.REDIS_PASSWORD, db: parseInt(process.env.REDIS_DB || '0', 10),
// Connection naming for monitoring connectionName: `${process.env.SERVICE_NAME || 'app'}-${process.env.HOSTNAME || 'local'}`,
// Connection management connectTimeout: 5000, // 5 seconds to establish connection commandTimeout: 3000, // 3 seconds for commands to complete keepAlive: 30000, // TCP keepalive every 30s
// Retry strategy - exponential backoff retryStrategy: (times) => { if (times > 10) { console.error('Redis connection failed after 10 retries') return null // Stop retrying } const delay = Math.min(times * 100, 3000) // Max 3s delay console.warn(`Redis reconnecting in ${delay}ms (attempt ${times})`) return delay },
// Offline queue management enableOfflineQueue: true, maxRetriesPerRequest: 3, // Fail fast after 3 retries
// Performance optimization enableAutoPipelining: true, // Batch commands automatically enableReadyCheck: true, // Wait for Redis to be ready
// Reconnection behavior autoResendUnfulfilledCommands: true, autoResubscribe: true}
const redis = new Redis(redisConfig)
// Event handlers for monitoringredis.on('connect', () => { console.log('Redis connecting...')})
redis.on('ready', () => { console.log('Redis ready')})
redis.on('error', (err) => { console.error('Redis error:', err)})
redis.on('close', () => { console.warn('Redis connection closed')})
redis.on('reconnecting', () => { console.log('Redis reconnecting...')})
export default redisKey Redis Configuration Choices
- connectTimeout: Don’t wait forever for a connection
- commandTimeout: Commands should complete quickly
- retryStrategy: Exponential backoff with a limit prevents infinite retry loops
- enableAutoPipelining: Batches commands for better performance
Step 5: Add Monitoring for Pool Health
I created a middleware to track pool metrics:
export function poolMetricsMiddleware(pgPool) { return (req, res, next) => { const stats = { pg: { total: pgPool.totalCount, idle: pgPool.idleCount, waiting: pgPool.waitingCount } }
// Add to request for logging req.poolStats = stats
// Alert if pool is stressed if (stats.pg.waiting > 5) { console.warn('PostgreSQL pool under pressure:', stats.pg) }
next() }}This helped me identify when the pool was running low on available connections, before it became a critical issue.
Step 6: Implement Graceful Shutdown
Properly closing connections during deployment prevents connection leaks:
import pgPool from './config/database.js'import redis from './config/redis.js'
async function gracefulShutdown(signal) { console.log(`Received ${signal}, closing connections...`)
try { await pgPool.end() console.log('PostgreSQL pool closed')
await redis.quit() console.log('Redis connection closed')
process.exit(0) } catch (err) { console.error('Error during shutdown:', err) process.exit(1) }}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))process.on('SIGINT', () => gracefulShutdown('SIGINT'))Bonus: Redis Connection Pool for High Throughput
For applications with high Redis throughput, a connection pool can distribute load:
import Redis from 'ioredis'
class RedisPool { constructor(options = {}) { this.poolSize = options.poolSize || 5 this.connections = [] this.currentIndex = 0 this.config = options.config || {}
// Initialize pool for (let i = 0; i < this.poolSize; i++) { const conn = new Redis({ ...this.config, connectionName: `${this.config.connectionName}-pool-${i}` }) this.connections.push(conn) } }
// Round-robin connection selection getConnection() { const conn = this.connections[this.currentIndex] this.currentIndex = (this.currentIndex + 1) % this.poolSize return conn }
// Proxy common commands async get(key) { return this.getConnection().get(key) }
async set(key, value, ...args) { return this.getConnection().set(key, value, ...args) }
async del(key) { return this.getConnection().del(key) }
// Cleanup async quit() { await Promise.all(this.connections.map(conn => conn.quit())) }}
// Usageconst redisPool = new RedisPool({ poolSize: 5, config: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379', 10), password: process.env.REDIS_PASSWORD, connectionName: 'myapp' }})
export default redisPoolThis distributes commands across multiple Redis connections, reducing contention.
The Complete Production Setup
Here’s a consolidated module for both databases:
import { Pool } from 'pg'import Redis from 'ioredis'
// PostgreSQL configurationexport function createPgPool() { const replicaCount = parseInt(process.env.REPLICA_COUNT || '1', 10) const maxDbConnections = parseInt(process.env.MAX_DB_CONNECTIONS || '100', 10) const poolSize = Math.max(Math.floor(maxDbConnections / replicaCount) - 5, 2)
const pool = new Pool({ max: poolSize, min: Math.max(Math.floor(poolSize * 0.3), 1), idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, maxLifetimeSeconds: 1800, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : false })
pool.on('error', (err) => { console.error('PostgreSQL pool error:', err) })
return pool}
// Redis configurationexport function createRedisClient() { return new Redis({ host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || '6379', 10), password: process.env.REDIS_PASSWORD, connectionName: process.env.SERVICE_NAME, connectTimeout: 5000, commandTimeout: 3000, keepAlive: 30000, retryStrategy: (times) => times > 10 ? null : Math.min(times * 100, 3000), maxRetriesPerRequest: 3, enableAutoPipelining: true })}
// Health check utilityexport async function checkDatabaseHealth(pgPool, redis) { const health = { postgresql: 'unhealthy', redis: 'unhealthy', timestamp: new Date().toISOString() }
try { await pgPool.query('SELECT 1') health.postgresql = 'healthy' } catch (err) { console.error('PostgreSQL health check failed:', err) }
try { await redis.ping() health.redis = 'healthy' } catch (err) { console.error('Redis health check failed:', err) }
return health}Production Checklist
Before deploying to production, verify:
- Calculate pool size based on replica count
- Set appropriate timeouts (connection, idle, command)
- Configure SSL for all connections
- Implement error handlers for both pools
- Add monitoring/metrics for pool health
- Test connection limits under load
- Implement graceful shutdown
- Document pool settings for your infrastructure
Key Takeaways
- Never use defaults in production - Default pool sizes (10 for pg) don’t account for multiple replicas
- Do the pool math - Calculate
pool_size = max_connections / replica_count - buffer - Set appropriate timeouts - Fail fast, don’t let connections hang
- Monitor pool health - Track total, idle, and waiting connections
- Handle errors gracefully - Pool errors shouldn’t crash your app
- Plan for scaling - As you add replicas, recalculate pool sizes
The production outage taught me an important lesson: connection pool configuration isn’t a “set and forget” task. It requires understanding your infrastructure, calculating based on actual constraints, and monitoring to catch issues before they become outages.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 node-postgres Pool Documentation
- 👨💻 ioredis API Documentation
- 👨💻 PostgreSQL max_connections Documentation
- 👨💻 Redis Connection Handling Best Practices
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments