Overview
A connection pool keeps connections open and lends them out. The database sees a small steady number of sessions, and requests skip the handshake. More connections do not mean more throughput; past a point they reduce it.
How it works
Application-level pool settings and the proxy layout.
# HikariCP style
maximumPoolSize=10
connectionTimeout=2000 # ms to wait for a free connection
maxLifetime=1800000 # recycle before server or LB closes it
200 app pods x 10 = 2000 connections -> too many
pods -> PgBouncer (transaction mode, 100 server conns) -> Postgres
A common starting size is roughly twice the CPU cores of the database server, shared across all clients.
Queueing theory explains the sizing. A database with sixteen cores completes a bounded number of queries concurrently, and extra connections only add context switching and lock contention. Little law says concurrency equals throughput times latency, so a service handling five hundred queries per second at ten milliseconds each needs about five connections in flight. Pools should therefore be small, with waiting requests time-limited, so overload turns into fast errors rather than a growing queue that hides the problem.
Pitfalls
- Sizing the pool per instance without multiplying by instance count.
- Forgetting to release connections on error paths, causing leaks.
- Holding a connection while calling slow external services.
- Using prepared statements or session variables through a transaction-mode pooler.
Key takeaways
- Total connections across all clients must fit the database.
- Always bound the wait and fail fast when the pool is empty.
- Keep transactions short so connections turn over quickly.
- Use a pooling proxy for serverless and large fleets.