Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

Connection Pool Sizer

A database connection pool sizer built on Little’s Law. Enter your peak queries per second, average query latency, and how many application instances you run, and get the concurrency you actually need, a per-instance pool size with headroom, and a check against your database’s max-connections limit — so you neither starve requests nor exhaust the database.

8connections / instance
48total across 6 instances
Concurrency (Little’s Law)30.0
✓ Within the DB capTotal 48 connections uses 24% of the database cap (200). Comfortable — bigger pools would add contention, not throughput.

A connection pool is sized by Little’s Law: the connections in use at once equal the query rate times how long each query holds a connection. This sizer applies that to your whole fleet, splits it across app instances, adds burst headroom, and checks the total against the database’s connection limit, because every instance brings its own pool to the same database.

What it computes

concurrency  = QPS × latency_ms ÷ 1,000
per_instance = max(1, ⌈concurrency ÷ instances × (1 + headroom ÷ 100)⌉)
total        = per_instance × instances
cap_used     = round(total ÷ db_max × 100) %
  • QPS is peak queries per second across the whole fleet, not per instance.
  • latency_ms is the average time a query holds a connection: execution plus the round trip. If code holds a connection for a whole transaction, use the transaction time.
  • instances is the number of app processes that each own a pool; the math treats anything below 1 as 1.
  • headroom is a percentage added to each instance’s share for bursts. The result rounds up, and no pool is smaller than one connection.
  • db_max is the database’s connection limit, such as PostgreSQL’s max_connections. A total above it turns the status to “Over the DB cap”; otherwise the status reports the share used.

A worked example

The defaults describe 2,000 queries per second at 15 ms, spread over 6 app instances, with 50% headroom and a database limit of 200.

  1. Concurrency: 2,000 × 15 ÷ 1,000 = 30.0 connections busy on average across the fleet.
  2. Per instance: 30 ÷ 6 = 5, then 5 × 1.5 = 7.5, rounded up to 8 connections per instance.
  3. Total: 8 × 6 = 48 connections, or 48 ÷ 200 = 24% of the cap, so the status reads “Within the DB cap”.
Change from the defaultsConcurrencyPer instanceTotalCap used
None30.084824%
Queries slow to 60 ms120.03018090%
Queries slow to 100 ms200.050300over the cap
Traffic doubles to 4,000 QPS60.0159045%
Scale out to 30 instances30.026030%

Two lessons sit in that table. Latency moves the pool as much as traffic does, so a database that slows from 15 ms to 100 ms needs more than six times the connections at the same QPS, exactly when it can least afford them. And scaling out inflates the total through rounding, because each of 30 instances rounds its 1.5-connection share up to 2.

Assumptions and limits

  • Even load. Traffic is assumed to split evenly across instances. An instance that receives more than its share needs a bigger pool than the average.
  • Average hold time. Little’s Law works on averages. Slow queries hold connections longer, and code that calls other services while a connection is checked out holds it far longer than the query itself.
  • Only this app. Migrations, cron jobs, admin consoles and other services draw from the same limit, and PostgreSQL reserves 3 connections for superusers by default.
  • No warning band. The status is either over or within the cap, so 99% still reads as within. Watch the percentage too.
  • A floor, not a performance target. The tool finds the connections your load needs. Whether the database can run that many queries at once without contention depends on its cores, disks and queries.

How to use the result

  • Set each pool’s maximum near the per-instance number. Once a pool covers real concurrency, extra connections add lock contention and memory pressure rather than throughput, and latency can get worse.
  • Budget for the most instances you will run. Use the autoscaler’s ceiling plus the extra instances a rolling deploy starts during the changeover, not today’s count.
  • Keep slack under the cap. A total that uses most of the limit leaves no room for the other clients above, and the next one to connect is refused.
  • Over the cap? Put a pooler in front. PgBouncer or a similar proxy multiplexes many client connections onto a few real ones, so app instances can scale without the database limit scaling with them.

How it works

  • Concurrency = QPS × latency (Little’s Law), across the whole fleet.
  • Per-instance pool = concurrency ÷ instances × (1 + headroom), rounded up, minimum 1.
  • Total = per-instance pool × instances, checked against the DB max-connections cap.
  • Bigger is not safer: size to real concurrency, not to the DB limit.

Frequently asked questions

How do you calculate database connection pool size?

Use Little’s Law: connections in use at once equal queries per second times the time each query holds a connection, in seconds. At 2,000 queries per second and 15 ms, that is 30 busy connections across the fleet. Divide by the number of app instances, add headroom for bursts and round up: 30 ÷ 6 instances × 1.5 is 7.5, so each instance gets a pool of 8. Multiply back by the instance count, 48 here, to see what the database has to accept.

Why check the total against the database max_connections?

Every application instance has its own pool, but they all connect to the same database. Twenty instances with a pool of 50 is up to 1,000 connections, and once the limit is reached the database refuses new ones. Connections also cost the server memory: in PostgreSQL each one is a separate backend process. The default max_connections in PostgreSQL is 100, lower than many teams assume, so check the multiplied total before a deploy or an autoscaling event finds the limit for you.

Is a bigger connection pool better?

No. Once the pool covers your real concurrency, extra connections add no throughput. They add lock contention, memory use and context switching on the database, and can make latency worse. The HikariCP project’s pool-sizing guidance makes the same argument, starting from a formula of the database server’s core count times two plus its effective spindle count for total active connections. Size to measured concurrency plus modest headroom, not to the largest number the database will accept.

What latency should I enter for pool sizing?

Enter how long a connection stays checked out of the pool, not just how long the query runs on the server. That includes the network round trip and any time your code holds the connection between statements. If requests open a transaction, call another service and then commit, the hold time is the whole transaction. Use the average at peak traffic, because query latency usually rises under load, and rerun the numbers with a slower value to see how close you get to the cap.

When should I use a pooler like PgBouncer?

When the multiplied total exceeds the database limit, or when instance counts swing widely with autoscaling or serverless functions. A pooler such as PgBouncer or ProxySQL accepts many client connections and multiplexes them onto a small number of real database connections, so app instances can scale without the database limit scaling with them. Transaction pooling multiplexes best, but it breaks session-level features such as SET commands and advisory locks held across transactions, so check what your driver and code rely on first.

How does autoscaling change the pool size?

The total is the per-instance pool times the instance count, so size for the most instances you will ever run, including the extra ones a rolling deploy starts before the old ones stop. Scaling out also inflates the total through rounding: at the defaults, 30 instances each round a 1.5-connection share up to 2, giving 60 connections where 6 instances needed 48. For large fleets, small per-instance pools behind a pooler usually beat larger ones that connect directly.