---
title: "Connection Pool Sizing: Why More Connections Make It Slower"
description: "Past the point where the server can execute work in parallel, adding connections adds queueing rather than throughput — and moves the queue somewhere worse."
url: https://sqlpress.com/blog/connection-pool-sizing/
category: PostgreSQL
tags: ["connection-pooling", "reliability", "query-optimization"]
author: "Elias Rowe"
published: 2026-10-04T00:00:00.000Z
measured: false
---

# Connection Pool Sizing: Why More Connections Make It Slower

## Key takeaways

- A database executes a bounded number of requests in parallel; connections beyond that number wait, whether they wait in the pool or inside the server.
- Waiting inside the server is worse, because each PostgreSQL connection is an operating system process with its own memory and scheduling cost.
- Throughput is bounded by the server; the pool only decides where the queue forms and how visible it is.

## Test environment

- System: PostgreSQL 18
- Workload: Illustrative (examples verified, nothing timed)
- Notes: Sizing reasoning assumes PostgreSQL's process-per-connection model, in which work_mem is allocated per sort or hash node per connection.

Raising the pool size is the standard response to database timeouts, and it usually
makes them worse. The reason is that the pool does not control how much work the
server can do. It controls where the queue forms.

## Why does a bigger pool not increase throughput?

A database server executes a limited number of requests genuinely in parallel. That
limit comes from CPU cores for compute-bound work and from the storage layer's
ability to service concurrent I/O for the rest. It is a property of the machine.

If that limit is sixteen and the pool allows two hundred concurrent connections,
the server does not do two hundred things at once. It does sixteen and interleaves
the rest. Total throughput is unchanged; the additional connections contribute
waiting, not work.

Little's Law states this precisely: for a system in steady state, concurrency
equals throughput multiplied by latency. Fix throughput at the server's capacity,
raise concurrency, and latency rises to match. The extra connections buy queueing
delay and nothing else.

## Why waiting inside PostgreSQL is expensive

Every PostgreSQL connection is a separate operating system process. That has
consequences an idle connection in a pool does not have.

Each backend holds its own memory, and `work_mem` is allocated per sort or hash
operation per connection — a query with three such nodes across two hundred
connections can commit six hundred times `work_mem` in the worst case. Each backend
participates in scheduling and in the shared structures the server uses to
coordinate, so more of them means more contention on those structures.

So two hundred connections mostly idle is not free, and two hundred connections
mostly active is actively harmful. This is also why the argument here is specific
to PostgreSQL: an engine that serves a connection with a thread rather than a
process pays a different price for an idle one, and the sizing conclusion does not
carry over unchanged.

Reducing the number of server-side backends without reducing application
concurrency is what a connection proxy is for, and how much reduction it delivers
depends on [which PgBouncer pooling mode is
configured](/blog/pgbouncer-pooling-modes/).

## Where the queue belongs

Given that the queue exists either way, it should form in the pool, in the
application, where it is cheap and observable.

A pool of sixteen with a bounded acquisition timeout produces a clear signal: a
request waited for a connection and failed after a stated interval. That is a
metric with a name, and it degrades predictably under load.

A pool of two hundred produces a database whose latency rises for everyone,
including requests that would have been fast, until something times out somewhere
less specific. The failure is worse and the diagnosis is harder.

Attribution suffers too. Under saturation, a statement slowed by contention
and a statement that is expensive on its own merits look alike from the outside,
and separating them means reading [what a plan reports about pages read and
hit](/blog/explain-analyze-buffers/) for that statement rather than watching the
server average.

## How many connections should the pool allow?

The frequently quoted starting point is roughly twice the core count, plus an
allowance for concurrent disk I/O. It is a heuristic and should be treated as one:
a place to begin, not a result.

The workload decides the other half of it. A request that issues [one query per
element of a collection](/blog/n-plus-one-queries/) holds its connection across
all of them, so the same pool serves fewer requests per second than the
individual query timings suggest.

The measurement that settles it is straightforward. Hold the workload constant and
vary only the pool size. Throughput rises with pool size, then flattens. Past the
flattening point, latency continues to climb while throughput does not. That inflection
is the answer for that workload on that hardware, and it moves when either changes.

What matters more than the exact number is the shape: it is a curve with a plateau,
not a line, and the whole class of "we increased the pool and it got slower" reports
comes from being on the far side of the plateau.

## Limits

This describes a single application talking to a single database. Multiple
services, background workers, and migration jobs each with their own pool share the
same server bound, and sizing each independently against the server's total capacity
is how instances end up with connection counts nobody chose. The server-side limit
is the one that has to add up.

## FAQ

### Why does increasing the connection pool size make PostgreSQL slower?

A database server executes a bounded number of requests genuinely in parallel, set by CPU cores for compute-bound work and by the storage layer for the rest. Connections beyond that bound do not add throughput, they add queueing. Little's Law makes it precise: hold throughput at the server's capacity, raise concurrency, and latency rises to match.

### How many connections should a PostgreSQL connection pool have?

The frequently quoted starting point is roughly twice the core count plus an allowance for concurrent disk I/O, and it is a heuristic rather than a result. The measurement that settles it holds the workload constant and varies only the pool size. Throughput rises, then flattens; that flattening point is the answer for that workload on that hardware, and it moves when either changes.

### Why are PostgreSQL connections more expensive than pooled connections?

Every PostgreSQL connection is a separate operating system process. Each backend holds its own memory, and work_mem is allocated per sort or hash operation per connection, so a query with three such nodes across two hundred connections can commit six hundred times work_mem in the worst case. Each backend also adds contention on the shared structures the server uses to coordinate.

### Is it better to queue in the connection pool or in the database?

The queue exists either way, so it should form in the pool, in the application, where it is cheap and observable. A small pool with a bounded acquisition timeout produces a named metric: a request waited for a connection and failed after a stated interval. A large pool instead raises latency for everyone, including requests that would have been fast.

## Sources

- [PostgreSQL 18 documentation — Connections and Authentication](https://www.postgresql.org/docs/18/runtime-config-connection.html) (docs)
- [PostgreSQL 18 documentation — Resource Consumption](https://www.postgresql.org/docs/18/runtime-config-resource.html) (docs) — work_mem is per sort or hash node per connection, not per server.
- [HikariCP — About Pool Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing) (article) — The widely cited starting formula; treat as a heuristic, not a measurement of your system.
- [PostgreSQL 18 documentation — Architectural Fundamentals](https://www.postgresql.org/docs/18/tutorial-arch.html) (docs) — The server forks a new process for each connection.
- [Little, J. D. C. — A Proof for the Queuing Formula: L = λW](https://doi.org/10.1287/opre.9.3.383) (paper) — Operations Research 9(3), 1961, 383-387. The proof holds regardless of arrival distribution, server count or queue discipline.
