How big should your database connection pool be?

Short answer: smaller than you think. The pool only needs as many connections as queries are running at the same time: throughput × time a query holds its connection (Little's law), plus headroom. For a database that serves from memory, that is a small multiple of its CPU cores; in our run, 4 connections were enough for 2 vCPUs.

We ran the same Postgres on 2 vCPU with 16 pooled connections and with 4. Both served the same ~5,950 requests/s at 100 % CPU. With 16, a query took up to 4× longer inside the database, because 16 queries shared 2 CPUs; with 4, the queries stayed fast and the waiting moved into the application's pool.

The method: Little's law

The average number of connections in use equals the rate of queries times how long each one holds its connection:

connections in use = queries per second × seconds per query (incl. network round trip)

5,760 queries/s × 0.6 ms  ≈ 3.5 connections busy on average

Size the pool a bit above the average at your peak, so bursts don't wait long: with random arrivals, a pool that is 70–80 % busy on average already makes some requests wait. Then check the other limit: the database can only run as many queries at once as it has cores (plus some that wait for disk). Connections beyond that do not add throughput; they queue inside the database instead of in your pool. The PostgreSQL wiki's rule of thumb, cores × 2 + effective disks, comes from the same reasoning (PostgreSQL wiki, HikariCP: About Pool Sizing).

The measurement

Measured 2026-09-30 (runs s2-poisson-base and s2-poisson-base-pool2). Query time = from sending the query to its result, as the app sees it: the connection is busy for that long. Pool wait = mean over both workers. At the last step the offered load exceeded capacity.
Offered req/sDB CPU 16 / 4Query time 16Query time 4Pool wait 16Pool wait 4p99 16 / 4
6409 / 9 %0.5 ms0.5 ms0.0 ms0.0 ms1.4 / 1.3 ms
1,92028 / 27 %0.5 ms0.5 ms0.0 ms0.0 ms1.6 / 1.5 ms
3,20049 / 47 %0.6 ms0.5 ms0.0 ms0.1 ms2.5 / 1.9 ms
3,84060 / 57 %0.7 ms0.5 ms0.0 ms0.2 ms3.5 / 3.0 ms
4,48069 / 67 %0.8 ms0.6 ms0.0 ms0.6 ms5.7 / 6.9 ms
4,80075 / 73 %0.9 ms0.5 ms0.0 ms1.0 ms6.9 / 6.6 ms
5,12080 / 79 %0.9 ms0.6 ms0.1 ms0.6 ms7.3 / 4.7 ms
5,44088 / 86 %1.4 ms0.6 ms0.3 ms1.6 ms12.6 / 10.4 ms
5,76094 / 93 %1.6 ms0.6 ms1.9 ms4.2 ms21.0 / 32.9 ms
6,08099 / 97 %2.6 ms0.6 ms38 ms40 ms1,966 / 1,375 ms

What the numbers say:

  1. Four connections were enough. Both runs reached the same throughput (5,947 and 5,958 requests/s at the last step) and the same database CPU. Little's law predicts it: at 5,760/s × 0.6 ms, about 3.5 connections are busy on average.
  2. More connections moved the queue into the database. With 16, the time a query held its connection grew from 0.5 to 2.6 ms as load rose: the extra queries were running concurrently and sharing 2 CPUs. With 4 it stayed at 0.5–0.6 ms, and requests waited for a free connection in the app instead.
  3. End-to-end p99 went back and forth. The wait has to happen somewhere. The small pool was lower at most steps up to 5,440/s (4.7 vs 7.3 ms at 5,120; higher only at 4,480: 6.9 vs 5.7 ms) and about 57 % higher at 5,760/s (32.9 vs 21.0 ms). In an earlier pair of runs the small pool's p99 rose sooner. Don't expect a smaller pool to make requests faster; expect it to keep the database healthy.

Why too many connections hurt Postgres

Limits of this measurement

Try it on your own design

In Stackrig every connection between two components can have a pool limit per caller instance. Set it, turn the traffic up, and see whether requests wait in your app or queries pile up in the database.

Open “Web app with database” in the playground Watch the live demo Get early access

More