Skip to content

Database Connection Pooling: PgBouncer & HikariCP

CoreConceptAugust 4, 20269 min read

Establishing a fresh TCP connection to a PostgreSQL database server requires a 3-way TCP handshake, TLS certificate negotiation, process fork (backend process creation in Postgres), and authentication verification. Each backend process in PostgreSQL consumes roughly 5MB to 10MB of host RAM. Opening and closing raw database connections on every incoming HTTP request degrades application throughput and crashes database servers during traffic spikes.

Database Connection Pooling maintains a warm set of reusable database connections. PgBouncer (the lightweight server-side connection pooler for PostgreSQL) multiplexes thousands of incoming client connections onto a tiny pool of backend server connections, while HikariCP provides the fastest client-side connection pooler for JVM applications. This guide details PgBouncer transaction pooling modes, HikariCP bytecode optimizations, pool sizing formulas, and memory overhead tuning.

Database connection pooling architecture featuring PgBouncer server-side transaction multiplexing, HikariCP client pooler, and pool sizing math
Database connection pooling architecture featuring PgBouncer server-side transaction multiplexing, HikariCP client pooler, and pool sizing math

Mental Model: Expensive Per-Request TCP Database Connections vs Multiplexed Connection Pools

Direct database connections force PostgreSQL to fork a dedicated backend process for every connected client. If 1,000 microservice pods connect directly, PostgreSQL attempts to fork 1,000 OS processes, exhausting host memory and causing heavy context-switching delays.

Multiplexed Connection Pooling Architecture decouples client connections from server backends:

1. Server-Side Proxy (PgBouncer): Accepts 5,000 client TCP connections, but multiplexes them down to just 50 active PostgreSQL server backend processes. 2. Client-Side Pooler (HikariCP): Manages an in-memory pool of pre-authenticated TCP connections, handing them to application threads in microseconds. For PostgreSQL scaling, review postgres partitioning vs sharding and optimizing database queries indexing strategies.

PgBouncer transaction pooling lifecycle showing client connection multiplexing, SQL transaction execution, and connection return
PgBouncer transaction pooling lifecycle showing client connection multiplexing, SQL transaction execution, and connection return

Quick reference

  • Pre-allocates a warm pool of authenticated database TCP connections to eliminate handshake overhead.
  • Prevents PostgreSQL RAM exhaustion by limiting active backend forked processes to optimal levels.
  • Multiplexes thousands of client application connections onto a small set of server connections.
  • Reduces query response latency from 50ms (connection handshake) to under 1ms.
  • Powers production database architecture at Instacart, Gitlab, Shopify, and CoreConcept.

Remember this

Deploy PgBouncer and HikariCP connection pools to multiplex client traffic and protect database RAM.

PgBouncer Transaction vs Session vs Statement Pooling Modes

PgBouncer operates in three distinct pooling modes configured via pgbouncer.ini:

- Transaction Pooling (Recommended): PgBouncer assigns a server connection to a client for the duration of a single SQL transaction (BEGIN ... COMMIT). Once committed, the server connection is returned to the pool instantly. Allows 10,000 client connections to share 100 Postgres connections. - Session Pooling: PgBouncer assigns a server connection when the client logs in and keeps it allocated until the client disconnects. Safest for applications relying on SET statements or temporary tables. - Statement Pooling: Re-allocates server connections for every individual SQL statement. Disallows multi-statement transactions.

Quick reference

  • Transaction Pooling mode yields the highest connection density by returning connections on COMMIT.
  • Requires avoiding session-level state (e.g. SET TIMEZONE, PREPARE statements, LISTEN/NOTIFY).
  • PgBouncer consumes under 2MB RAM per 1,000 client connections due to event-driven libevent core.
  • Supports TLS client-side and server-side encryption for secure database proxying.
  • Max client connections (max_client_conn) can be set to 10,000+ without degrading database RAM.

Remember this

Use PgBouncer Transaction Pooling for maximum client connection density and sub-1ms routing.

Client-Side HikariCP Thread-Safety, Lock-Free Bytecode, & Leak Detection

HikariCP is engineered for extreme JVM performance using lock-free data structures (FastList, ConcurrentBag):

1// HikariCP Configuration2HikariConfig config = new HikariConfig();3config.setJdbcUrl("jdbc:postgresql://pgbouncer.internal:6432/production");4config.setUsername("db_user");5config.setPassword("secret");6config.setMaximumPoolSize(20);7config.setMinimumIdle(10);8config.setLeakDetectionThreshold(2000); // Alert if thread holds connection >2s!9 10HikariDataSource ds = new HikariDataSource(config);
PgBouncer transaction pooling lifecycle showing client connection multiplexing, SQL transaction execution, and connection return
PgBouncer transaction pooling lifecycle showing client connection multiplexing, SQL transaction execution, and connection return

Quick reference

  • Uses lock-free ConcurrentBag and FastList data structures to eliminate Java synchronized lock overhead.
  • Bytecode-level optimizations (Javassist) inline connection checks for nanosecond execution speeds.
  • LeakDetectionThreshold logs stack traces if an application thread holds a connection without closing it.
  • MinimumIdle and MaximumPoolSize parameters tune dynamic connection elasticity.
  • Automatically validates connection health before handing connections to application threads.

Remember this

Configure HikariCP with leakDetectionThreshold to catch unclosed database connections in application code.

Sizing Connection Pools: Formula Math, Max Connections, & CPU Core Scaling

A common anti-pattern is setting connection pool sizes to 200 or 500 connections per application instance. Oversized pools cause disk I/O thrashing and CPU context-switching overhead on database servers.

### The PostgreSQL Connection Sizing Formula PostgreSQL core developers recommend the official sizing formula:

$$\text{connections} = (\text{CPU cores} \times 2) + \text{spindle count}$$

For a database server with 16 CPU cores and SSD storage: $$\text{connections} = (16 \times 2) + 1 = 33 \text{ max active connections}!$$

A small, fully saturated connection pool of 33 connections processes requests significantly faster than an overloaded pool of 500 queued connections.

Quick reference

  • Follow the formula (CPU cores * 2) + spindle count to determine optimal active DB connection limits.
  • Oversized connection pools create severe CPU context-switching and disk I/O thrashing.
  • A pool of 30 active connections frequently out-performs 500 connections under heavy concurrency.
  • Configure PgBouncer default_pool_size to match the formula output strictly.
  • Monitors pool queue wait duration (pool_wait_time) in Grafana to determine if sizing adjustments are needed.

Remember this

Size database connection pools using the (CPU cores * 2) + 1 formula to avoid CPU thrashing.

Key takeaway

To test PgBouncer locally, run docker run -d -p 6432:6432 edoburu/pgbouncer. Point HikariCP to port 6432 and benchmark transaction throughput using pgbench.

Share:

Related Articles

At the heart of every database system lies a Storage Engine that determines how data is written to disk, indexed, and re

Read

Database queries executing against multi-million row tables suffer severe latency spikes if the database storage engine

Read

As relational databases grow beyond millions to billions of rows, single-table query performance degrades due to massive

Read

Keep learning

Follow a structured path or browse all courses to go deeper.