Skip to content
Aitsam Ahad

Software Engineering

How big should your connection pool actually be?

Your app is slow, your database CPU is idle, and every request is hanging. The instinct is to raise the connection pool size.

faster with fewer connections — faster with FEWER connections

A team cut their connection pool and response times went from around 100ms to around 2ms. Same hardware. Same queries. Same code. The only change was allowing fewer things to happen at once.

That is not a tuning trick. It is what queueing theory predicts, and the reason it keeps surprising people is that the instinct it violates feels like common sense.

response time, same hardware and code
Roughly 50× faster, from removing capacity.

Why it matters

"More connections means more throughput" is one of those beliefs that is almost never stated out loud, which is exactly why it never gets examined. It is baked into defaults, into tutorials, and into the shape of most incident postmortems.

The cost of getting it wrong is not a slightly slower service. It is a specific and violent failure mode that looks like a database problem while the database sits nearly idle.

The mental model

The number of connections you actually need is not a matter of taste. It comes out of Little's Law:

text
L = λW

where L is the average number of items in a system, λ is the arrival rate, and W is the average time each item spends inside.

connections actually needed at 20 req/s, 200ms each
What the arithmetic asks for, against what is usually configured.

At 20 requests per second, each holding a connection for 200ms:

text
L = 20 × 0.2 = 4 connections

Four. The typical configuration in that situation is 200 — a factor of fifty more than the work requires.

And the law cuts both ways, which is the part worth internalising:

same traffic, slower queries
Ten times the query time, ten times the connections, identical traffic.
query time arrival rate connections needed
200ms 20 req/s 4
2s 20 req/s 40

Nothing about the traffic changed. The pool requirement moved by 10× because W did. If your pool size is a constant, it is only correct for one value of W, and query time is the thing that moves most in an incident.

The mechanism

Past the point where every CPU core has work, additional concurrency does not add throughput. It adds context switching, lock contention and cache pressure — so the server does the same amount of useful work while spending more of its time on coordination.

The HikariCP pool-sizing guide gives the formula that falls out of this:

text
connections = ((core_count × 2) + effective_spindle_count)

// 4-core server, one spinning disk:
//   (4 × 2) + 1  =  9 connections

Nine. The "× 2" is there because a thread is not using its core while it waits on I/O, so a little oversubscription keeps the cores fed. It is not there to let you scale the number up until it feels generous.

The failure mode

This is where an oversized pool stops being merely wasteful and becomes actively dangerous.

the death spiral
How an oversized pool turns a slowdown into an outage.
t+0s pool fully checked out
t+1s requests queue, then time out
t+2s clients retry — demand doubles
t+3s the database is thrashing, not working

The retries are the detail that makes it a spiral rather than a dip. Every timed-out request becomes two requests. A large pool makes this worse than a small one, because it lets far more concurrent work reach a database that is already past the point of doing anything useful with it.

A smaller pool fails differently, and better. Requests wait in your application, where waiting is cheap, instead of waiting inside the database, where it is not.

Back to the anomaly

active connections vs max
A healthy pool turns over. A sick one is pinned.

The 50× improvement was not the pool getting faster. It was the database being allowed to finish things. With fewer connections competing, each query completed sooner, W fell, and by Little's Law the number of connections needed fell with it — which left the pool with headroom it had never had before.

What to actually watch

Pool size tells you almost nothing. The number worth alerting on is how long a thread waits to be handed a connection:

text
hikaricp_connections_acquire_nanos

// healthy: essentially zero — a connection is waiting for you
// rising:  threads are queueing for the pool

Exposed by the HikariCP integration and equivalents elsewhere. If acquire time is flat at zero and your service is slow, the pool is not your problem and making it bigger will not help.

Where else this applies

The same arithmetic governs thread pools, worker queues, HTTP client concurrency limits and rate limiters. Anywhere a fixed number of servers process arriving work, L = λW holds, and "add more workers" stops helping at the point where the underlying resource saturates.

There is one genuine exception worth knowing, because it is the case where a larger pool is correct:

text
pool size = Tn × (Cm - 1) + 1

// Tn = max threads
// Cm = max simultaneous connections held by one thread

If a single thread can hold more than one connection at a time — nested transactions, a query issued while another is open — a pool sized purely for throughput can deadlock, with every connection held by a thread waiting for a connection that will never come free. This formula guarantees at least one thread can always make progress.

What to look at next

Measure your actual W under load, not at rest, and compute λW for your real arrival rate. Then compare that to your configured maximum. In most services the gap is an order of magnitude or more.

The question worth asking next: what happens to that number when the queue itself becomes the bottleneck?

Sources

  • Connection Pools
  • Performance
  • Queueing

Written by

Aitsam Ahad

Senior Full-Stack Engineer with 6+ years architecting scalable web applications in Node.js, TypeScript, Express and NestJS on the backend and React/Next.js on the front. Currently Principal Software Engineer at TEO International, Islamabad.

Explore my experience