Module A-14·25 min read

PM2 graceful reload, pg-pool sizing mathematics, Redis Cluster client configuration, and Kubernetes liveness vs readiness probe implementation.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module 13 — Advanced Connection Pooling & Process Management

What this module covers: Your Node.js service processes 50,000 events/second. Every event requires a database write. That means 50,000 PostgreSQL queries/second. A naive implementation opens a new connection for every query — connection establishment costs 5–10ms and PostgreSQL cannot handle 50,000 simultaneous connections. Connection pooling is the mechanism that multiplexes thousands of queries over dozens of connections. This module covers pg-pool sizing mathematics, Redis connection management, PM2 cluster lifecycle, graceful shutdown sequences, and the Kubernetes liveness vs readiness probe distinction that prevents traffic routing before your service is ready.


Why Connection Pooling Is Not Optional

Every database connection is expensive:

  • PostgreSQL: spawns a new OS process per connection (~5MB RAM, ~10ms setup cost)
  • Redis: opens a new TCP socket per connection (~0.5ms setup, persistent)
  • MongoDB: opens a new socket, negotiates auth (~2ms)

Without pooling at 50,000 queries/second:

  • 50,000 new PostgreSQL connections/second × 10ms = 500 seconds of connection overhead per second of operation — impossible
  • At any instant, thousands of concurrent queries would require thousands of simultaneous PostgreSQL processes: 5,000 connections × 5MB = 25GB RAM just for PostgreSQL process overhead

Connection pooling solves this by reusing a small set of long-lived connections for many short-lived queries.


pg-pool: PostgreSQL Connection Pool

pg-pool (used internally by the pg package) maintains a pool of PostgreSQL connections.

The Mathematics of Pool Sizing

A connection pool is the taxi rank outside a stadium after the final whistle — too few cabs and the queue snakes around the block even though every individual ride is quick.

text

This is Little's Law applied to database connections. If you need more throughput or have longer queries, you need more connections — up to the database's limit.

javascript

Monitoring Pool State

javascript

Alert thresholds:

  • db_pool_utilization_ratio > 0.8: pool approaching capacity
  • db_pool_waiting_count > 0: pool exhausted — immediate action needed
  • db_pool_waiting_count > 50: serious saturation — likely causing timeouts

Pool Exhaustion: The Silent Performance Killer

When all pool connections are busy, pool.connect() waits. At high throughput, this creates a latency cascade: query takes 15ms → pool slot held for 15ms → at 50 queries/pool × 15ms = 750ms of total query time per second → pool fully booked at 3,333 queries/second.

If your target is 5,000 queries/second:

required_pool_size = ceil(5,000 × 0.015) = 75 connections

Insufficient pool size manifests as:

  • P99 latency spikes (queries waiting for a connection)
  • connectionTimeoutMillis errors (pool exhausted for too long)
  • Not as high CPU or network errors — the service appears healthy from the outside while queries queue silently

Production story: During a festival traffic spike, a UPI payment service scaled its Kubernetes deployment from 4 replicas to 8 to absorb load, each pod running the max: 50 pool from the example above. Nobody re-ran the pool-sizing math against the replica count: 8 × 50 = 400 potential simultaneous connections to a single PostgreSQL instance configured with the default max_connections of 100. Within seconds of the new pods coming up, Postgres started rejecting new connections cluster-wide with FATAL: sorry, too many clients already — including from pods that had been running fine, because the limit is on the database, not per-service. The fix was PgBouncer in front of Postgres (below), decoupling "connections the app thinks it has" from "connections Postgres actually has to hold open."

The Missing Piece: PgBouncer (or an Equivalent External Pooler)

max: 50 on a single pod's pg.Pool is a per-process limit. It says nothing about how many processes there are. Run 8 replicas of the same service, each with max: 50, and the true ceiling against Postgres is 400 simultaneous connections — against a max_connections default that ranges from 100 to 300 depending on the managed Postgres provider. Every example pool configuration in this module has assumed a single process; at any real replica count that assumption breaks.

PgBouncer sits between your application and Postgres as a lightweight external pooler, in transaction pooling mode: it maintains a small number of real Postgres connections and multiplexes many more client connections onto them, handing a real connection to a client only for the duration of a single transaction.

text
ini

The trade-off: transaction pooling means session-level state (SET commands, advisory locks held across statements, LISTEN/NOTIFY) does not survive between queries from the same client connection, because the underlying Postgres connection can be handed to a different client between transactions. Anything in this module relying on session state (like maxUses recycling) needs to be re-evaluated against that constraint before adopting PgBouncer.


Connection Lifecycle: maxUses and Health Checks

Long-lived database connections accumulate state: temporary tables (from past SET commands), prepared statements, and sometimes server-side session state.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.