Why does putting a connection pool in front of a database make a service faster when the database does exactly the same query work either way?
Show the full answer Hide the answer
The mechanism
The query is not where the time went. Opening a connection is a conversation that happens before any SQL is sent, and a service that opens one per request pays for that conversation on every request.
On a 1 ms network path to a relational database the sequence is roughly: TCP handshake, one round trip. TLS handshake, one or two more. Then the protocol's own startup exchange and authentication, another round trip or two. Then, on a process-per-connection server such as Postgres, the server forks a new backend process, which costs a few milliseconds of CPU and allocates its own private memory and catalogue caches.
That is roughly 5–15 ms of setup before the first byte of a query that may itself take 2 ms. A pool makes the setup happen once per connection rather than once per request, so the per-request cost collapses to a checkout from an in-memory list.
There is a second effect that matters more at scale. A fresh connection starts with a cold server-side state: no cached query plans, no prepared statements, no warmed catalogue lookups. A pooled connection has already paid for those, so repeated statements skip planning entirely.
What the pool actually caps
The pool is also a concurrency limiter wearing a different hat. A pool of 40 slots means at most 40 statements are in flight at the database no matter how many requests arrive, because request 41 waits for a slot. That bound is the reason pools prevent outages as often as they cause them.
The quantity that forces the issue: a Postgres backend process costs roughly 5–10 MB of resident memory plus its share of scheduler and lock-manager work. Several hundred direct connections per database is already gigabytes of memory doing no useful work, which is why a transaction-mode proxy such as PgBouncer exists — it lets thousands of client connections share tens of server connections.
The failure mode pooling introduces
A pooled connection carries session state that a fresh connection would never have: SET statements, temporary
tables, advisory locks, prepared statements, an aborted transaction left open. A pool that returns a dirty
connection produces bugs whose cause is which request ran before yours, which is close to undebuggable from
logs. Any pool worth using resets or discards a connection on return, and the setting that does it is the one
people turn off for performance.
When the simpler alternative wins
- A single long-running process — a nightly batch job, a migration runner — should open one connection and keep it. A pool adds configuration and a failure mode and saves nothing.
- A driver that multiplexes requests over one connection already has the benefit; adding a pool on top just multiplies idle sockets.
- A serverless function with a short life is the case where a pool is actively harmful: each instance holds its own pool, so the connection count scales with instance count rather than with request rate. The answer there is an external proxy or a data API, not a bigger pool.
Common weak answers
- "Connections are expensive." True and insufficient. Say which part is expensive and roughly how much: round trips for the handshakes, process creation on the server, and a cold plan cache.
- "The pool makes queries faster." It makes the query no faster at all. It removes work that surrounds the query and it caps how much of that work runs at once.
- "Set the pool as large as possible." The cap is the point. A pool larger than the database can serve concurrently converts a fast rejection into a slow queue inside the database, where you cannot see it.