Skip to main content

On This Page

Optimizing Node.js and PostgreSQL: Solving Connection Exhaustion with PgBouncer

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Your Node.js App Is Probably Killing Your PostgreSQL (Connection Pooling Explained)

Node.js applications often trigger Out-of-Memory (OOM) kills on PostgreSQL servers by maintaining hundreds of idle backend processes. Each PostgreSQL connection spawns a dedicated process consuming roughly 5-10MB of RAM regardless of query activity.

Why This Matters

In ideal models, developers assume database connections are lightweight, but the technical reality of PostgreSQL’s process-per-connection architecture creates significant overhead. A modest setup of 75 idle connections can consume nearly 2GB of RAM, leaving insufficient memory for query execution, shared buffers, and work memory on standard 4GB servers, eventually leading to connection limit errors and latency spikes.

Key Insights

  • PostgreSQL backend processes consume 5-10MB of RAM each, meaning 280 connections can consume approximately 1.96GB of RAM.
  • Transaction pooling in PgBouncer allows multiplexing hundreds of client connections onto a small pool of server connections, reducing peak RAM usage from 1.47GB to 175MB.
  • Standard parameterized queries in the Node.js ‘pg’ driver are compatible with transaction pooling, whereas named prepared statements require session persistence and will fail.
  • Managed database proxies like RDS Proxy and Supavisor automate connection management, but require specific client configurations like the ‘?pgbouncer=true’ flag for Prisma.
  • Lowering max_connections in PostgreSQL after implementing a pooler allows more RAM to be allocated to shared_buffers and work_mem, directly improving query performance.

Working Examples

Docker Compose configuration for PgBouncer in transaction pooling mode.

services:
  pgbouncer:
    image: bitnami/pgbouncer:latest
    environment:
      POSTGRESQL_HOST: postgres
      PGBOUNCER_POOL_MODE: transaction
      PGBOUNCER_MAX_CLIENT_CONN: 1000
      PGBOUNCER_DEFAULT_POOL_SIZE: 25

Updated Node.js pool configuration pointing to the PgBouncer port.

const pool = new Pool({
  connectionString: "postgresql://app_user:password@pgbouncer:6432/myapp",
  max: 25,
});

Essential connection string flag for Prisma users utilizing transaction pooling.

DATABASE_URL="postgresql://user:password@pgbouncer:6432/myapp?pgbouncer=true"

Practical Applications

  • Use Case: Scaling Node.js microservices where 3 replicas of multiple services (web, workers, jobs) aggregate to exceed the 100-connection default. Pitfall: Increasing max_connections in PostgreSQL, which leads to RAM pressure and increased context switching.
  • Use Case: Implementing Row-Level Security (RLS) using set_config(‘app.organization_id’, orgId, true) to maintain transaction-scoped state. Pitfall: Using session-level SET statements that do not persist correctly across multiplexed connections.
  • Use Case: Managing real-time events with LISTEN/NOTIFY by bypassing the pooler with a direct connection. Pitfall: Attempting to use pub/sub over PgBouncer in transaction mode, which causes subscription loss when the connection is returned to the pool.

References:

Continue reading

Next article

Meta AI's EUPE: A <100M Parameter Universal Vision Encoder Rivaling Specialists

Related Content