PM2 graceful reload, pg-pool sizing mathematics, Redis Cluster client configuration, and Kubernetes liveness vs readiness probe implementation.
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
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.
Monitoring Pool State
Alert thresholds:
db_pool_utilization_ratio > 0.8: pool approaching capacitydb_pool_waiting_count > 0: pool exhausted — immediate action neededdb_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)
connectionTimeoutMilliserrors (pool exhausted for too long)- Not as high CPU or network errors — the service appears healthy from the outside while queries queue silently
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.
maxUses: 7500 causes pg-pool to destroy and recreate a connection after 7,500 uses. This prevents state accumulation without the overhead of creating a new connection per query.
Redis: Connection Management with ioredis
Redis connections are cheaper than PostgreSQL but still need pooling for high-throughput usage.
Pipelining: Batching Redis Commands
PM2: Process Management in Production
PM2 manages Node.js processes: starts them, restarts them on crash, monitors memory usage, and provides zero-downtime reload.
Ecosystem Configuration
Zero-Downtime Reload vs Restart
How reload works:
- PM2 starts a new worker instance
- New worker starts, begins accepting connections (via
SO_REUSEPORT) - PM2 sends
SIGTERMto old worker - Old worker: stops accepting new connections, finishes in-flight requests
- Old worker exits cleanly
- Repeat for each worker in the cluster
This is why implementing graceful shutdown correctly is essential for pm2 reload to work.
Graceful Shutdown: The Complete Sequence
A Node.js service has multiple resources that must be closed in the correct order on shutdown.
Kubernetes: Liveness vs Readiness Probes
Kubernetes uses two types of health probes with critically different semantics.
Liveness probe: "Is this pod still alive and should it be restarted?"
- If liveness fails: Kubernetes kills and restarts the pod
- Use for: detecting deadlocks, infinite loops, hung processes
- Be conservative: don't check dependencies (database) in liveness
Readiness probe: "Is this pod ready to receive traffic?"
- If readiness fails: Kubernetes removes the pod from load balancer rotation (but does NOT restart it)
- Use for: checking database connectivity, cache warmup, dependency availability
- A pod can be live but not ready (database is down → no traffic, but no restart needed)
The startup scenario:
Production Incident: Pool Exhaustion Masking as Latency Spike
Context: A UPI payment service with max: 20 connections in the pool, targeting 3,000 queries/second at 8ms average query time.
The math check no one did:
What happened: During normal operation, the service handled load because average query time was actually 6ms (not 8ms as designed for). Required pool: 3,000 × 0.006 = 18 connections. The pool of 20 had a 10% safety margin.
During a marketing push, traffic doubled to 6,000 queries/second. Required pool: 6,000 × 0.006 = 36 connections. Available: 20.
Symptom: p99 latency jumped from 12ms to 5,200ms (the connection timeout). CPU was at 30%. Database was at 25% capacity. The bottleneck was invisible without monitoring db_pool_waiting_count.
The fix:
Additionally, connectionTimeoutMillis was reduced from 5,000ms to 2,000ms. The long timeout was hiding the exhaustion — queries waited 5 seconds before failing, making the P99 look catastrophic while masking the root cause (pool size).
Summary
| Concept | Key Takeaway |
|---|---|
| Pool sizing | pool_size = target_qps × avg_query_duration_sec. Recheck when traffic grows. |
db_pool_waiting_count | First metric to add. Any waiting = pool undersized for current load. |
connectionTimeoutMillis | Set to 2–3s. Long timeouts hide exhaustion and make P99 look catastrophic. |
maxUses | Recycle connections after N uses to prevent server-side state accumulation. |
| ioredis pipelining | Batch Redis commands in one RTT. Use pipeline() for multi-key operations. |
PM2 pm2 reload | Rolling restart with zero downtime. Requires correct graceful shutdown implementation. |
| Graceful shutdown | Stop accepting → drain Kafka → drain DB pool → close Redis → exit. Hard timeout as fallback. |
| Liveness probe | Is the process alive? Check: process responsiveness only. Never check dependencies. |
| Readiness probe | Is the service ready for traffic? Check: database, Redis, Kafka. Fail gracefully during startup. |
pool.end() | Waits for all active queries to complete before closing connections. Safe for shutdown. |
The connection layer determines capacity. Module 14 covers the other end of the scale — ultra-lightweight V8 isolates for edge deployments that need zero cold starts and globally distributed intake proxies.
Next: Module 14 — Edge Runtime Ingestion & V8 Isolates →
When configuring a Kubernetes deployment for a Node.js service, what is the critical difference between a liveness probe and a readiness probe, and what should be checked in each?
A Node.js service has an average database query duration of 15ms. During a marketing event, traffic is expected to spike to 4,000 database queries per second. According to Little's Law applied to pool sizing, what is the mathematical minimum PostgreSQL connection pool size required to handle this throughput without queuing?
Why is it crucial to set a relatively short connectionTimeoutMillis (e.g., 2,000ms) on the PostgreSQL connection pool rather than a long default (e.g., 30 seconds)?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion