← Writing

Connection Pool Tuning for Node.js + PostgreSQL Under Load

Why Pools Matter

Each PostgreSQL connection costs:

  • ~5-10MB of server RAM
  • A dedicated process
  • TCP setup overhead

At 10,000 concurrent connections, PostgreSQL might hit memory limits or process limits before request limits.

Connection pools let you reuse a fixed set of connections across many application instances.

Pool Configuration

Using pg library for Node.js:

const { Pool } = require('pg');

const pool = new Pool({
  max: 20,                 // Max connections to keep open
  min: 5,                  // Min idle connections to maintain
  idleTimeoutMillis: 30000, // Idle connection TTL (30s)
  connectionTimeoutMillis: 5000, // Timeout waiting for available connection
});

Tuning for Load

Rule of Thumb

Pool Size = (Num Cores × 2) + Effective Spindle Count

For a 4-core machine: (4 × 2) + 0 = 8 connections.

For databases with SSD: use 2 × cores = 8 connections.

Under Load: Requests Timeout

Symptom: "getConnection() timeout exceeded"

const pool = new Pool({
  max: 20,
  min: 10,  // Increase min idle connections
  connectionTimeoutMillis: 10000, // Increase timeout to 10s
});

Monitor connection utilization:

setInterval(() => {
  console.log(`Pool: ${pool.totalCount} total, ${pool.idleCount} idle`);
  if (pool.idleCount === 0) {
    console.warn('Pool exhausted, increasing max');
  }
}, 5000);

Idle Connections Pile Up

Symptom: select count(*) from pg_stat_activity shows 1000+ connections.

Culprit: Long idleTimeoutMillis or applications that never close connections.

const pool = new Pool({
  idleTimeoutMillis: 10000, // Reduce from 30s to 10s
  connectionTimeoutMillis: 3000, // Aggressive timeout
  statement_timeout: '30s', // Database-side timeout (prevent hanging queries)
});

Query Hangs Block the Pool

If a query hangs, the connection is held until timeout. Every waiting request consumes memory and a slot from the pool.

// Add timeout to queries
const result = await pool.query(
  { text: 'SELECT * FROM large_table', timeout: 5000 },
  [userId]
);

Monitoring

Key metrics:

  • Connections active: number of queries in progress
  • Connections idle: available connections
  • Queue depth: requests waiting for a connection
  • Connection latency: time to acquire a connection

Example Prometheus metrics:

const prometheus = require('prom-client');

const poolTotal = new prometheus.Gauge({
  name: 'pg_pool_total_connections',
  help: 'Total connections in pool',
});

const poolIdle = new prometheus.Gauge({
  name: 'pg_pool_idle_connections',
  help: 'Idle connections available',
});

setInterval(() => {
  poolTotal.set(pool.totalCount);
  poolIdle.set(pool.idleCount);
}, 5000);

Checklist

  • Set max = 2 × CPU cores as baseline
  • Set min = 1/2 of max (reduce startup time)
  • Set idleTimeoutMillis = 10–30s (avoid idle connection waste)
  • Set connectionTimeoutMillis = 5s (fail fast on pool exhaustion)
  • Add database statement_timeout (prevent hanging queries from blocking pool)
  • Monitor pool.totalCount and pool.idleCount in production
  • Alert when idleCount === 0 (pool exhaustion)
  • Set up slow query logging (queries > 1s)