Why a bigger pool makes the database slower
Every connection to PostgreSQL is a backend process. Once the number of connections actively running queries passes the number of cores, those processes stop doing work and start taking turns. You pay for the context switching, the lock contention and the cache pressure, and you get nothing back.
The measured curve above comes from a real benchmark rather than a rule of thumb. PostgreSQL 17.11 limited to 4 cores, pgbench at scale 50, connection count swept from 1 to 256. Throughput peaked at 48 connections with 18,039 transactions per second. At 256 connections it was 11,715 — thirty-five percent lower — and average latency had gone from 2.66 ms to 20.96 ms. Pushing all the way to the configuredmax_connections of 400 cost 37% of throughput and multiplied latency by 13.
What about the (cores × 2) + spindles formula?
It gave 9 connections for this machine. The measured optimum was 48, five times higher. The formula assumes a connection is busy on CPU for its whole life; a transaction that spends part of its time waiting on WAL, the network or a lock leaves the core free for somebody else, and you need more connections to keep the cores fed. Treat the formula as a floor, not an answer, and measure your own workload.
Where PgBouncer fits
Transaction pooling breaks the link between how many connections your applications hold and how many the database sees. Four hundred clients can share twenty server connections because a connection is only assigned for the duration of a transaction. Turn the toggle on above and watch the database-side number collapse while throughput stays where it was.
This is the standard answer for serverless too. Lambda scales to hundreds of concurrent executions and each one opens its own connection, which is how a db.t3.mediumwith a default limit near 100 gets exhausted in seconds. Pooling inside the function does not help, because every new execution environment builds its own pool.
How to read your own numbers
Watch queue depth rather than connection count. In PgBouncer that is cl_waitingfrom SHOW POOLS; anything consistently above zero means clients are queuing. In HikariCP it is the pending-threads gauge. If queue depth is zero and the database is not saturated, your pool is already big enough — adding to it will move you to the right of the peak, and the graph above shows what that costs.


