A pool lends a few open connections and queues the rest
Borrow, run, return. When all are busy, callers wait; past the checkout timeout they get an error instead.
100% CPU. Add connections?
Bad: Raise max_connections to 5,000
Same cores. The extra queries just queue inside the database.
Good: Shrink the pool
One demo cut 2,048 connections to 96: 100 ms became 2 ms.
4,000
connections from 200 pods with pools of 20
That’s 40 times PostgreSQL’s default limit of 100, and autoscaling adds pods exactly when the database is busiest.
PgBouncer in transaction mode: 4,000 clients, 40 server connections
Most web connections sit idle between transactions, so PgBouncer lends a server connection per transaction, not per session.
With transaction pooling on, client A runs SET on server connection 1, then commits.
The catch
Client C’s next transaction lands on that connection and inherits A’s setting. Use SET LOCAL, or ALTER ROLE … SET.
Size it with Little’s law
- Busy connections = queries/s × hold time
- 5,000/s × 4 ms = 20 busy
- Add headroom, about 30 in total
- Cross-check: cores × 2 + spindles
A slow database drains the pool in half a second
Queries slow from 4 to 400 ms. The pool now serves 50 requests a second; 450 queue, and 200 threads block in 0.44 s.
Remember
Never connect per request. Size the pool with Little’s law, funnel pods through PgBouncer, and fail fast when it’s empty.