2026-05-19
Every database connection costs more than you think. A fresh PostgreSQL connection requires a TCP handshake, TLS negotiation, authentication, and backend process fork — typically 20-50ms before you've sent a single byte of SQL. Your average query might take 2ms. You're spending 10-25x more on setup than on actual work.
A connection pool keeps a set of pre-established connections warm and hands them out to application threads on demand. When the thread finishes, the connection returns to the pool instead of being destroyed.
The three knobs that matter:
The rule of thumb for sizing: connections = ((core_count × 2) + effective_spindle_count). For a typical 8-core app server hitting SSD-backed Postgres, that's around 17 connections. Most developers reflexively set max pool to 100 and wonder why their database melts under load. More connections is not more throughput — past a certain point, the database spends more time context-switching than executing queries.
Real-world example: A team I worked with had 20 app instances, each with a pool size of 50, hitting a Postgres instance configured for 200 max connections. Under load, half the app instances couldn't get connections at all, and the database was at 100% CPU thrashing between 200 backend processes. We dropped pool size to 10 per instance (200 total, matching DB capacity), latency dropped 40%, and p99 stopped spiking. Less was more.
Watch for these pitfalls:
server_check_query, HikariCP's connectionTestQuery) to avoid serving dead socketsAdd a layer when needed: For high-fanout architectures (many small services, serverless functions), put PgBouncer in transaction-pooling mode between your apps and the database. It multiplexes thousands of client connections onto dozens of real backend connections.
