skip to content

A relational database server shows plenty of idle CPU and free memory, yet query latency degrades badly after the application tier grows from 20 instances to 200, each holding its own connection pool. Explain what limits the number of usable database connections and how connection pooling changes the picture.

level: seniorimportance: should knowfreq 50%

answer

  1. connection ≠ throughput unit
  2. useful concurrency ≈ cores + IO parallelism
  3. 200 instances × pool 20 = 4000
  4. transaction-mode pooler multiplexes; breaks session state
  5. smaller pool can raise throughput

basics

~20 s

Each connection costs memory and a scheduling slot, and concurrency beyond the number of cores and disks only adds context switching, lock contention, and cache thrash. Throughput plateaus then falls. Fix it with a small pool per instance, or a shared external pooler that multiplexes many clients onto few server connections.

solid answer

~60 s

Connections are not free and are not the unit of throughput. Each server-side connection consumes memory (session state, per-operation working buffers, plan caches) and, in process- or thread-per-connection engines, an OS scheduling entity. Useful concurrency is roughly bounded by cores plus effective disk parallelism; beyond that, extra concurrent statements do not run faster — they interleave, thrash CPU caches, lengthen lock queues, and increase the chance of contention and deadlocks. Throughput flattens and then declines while latency climbs, which is exactly the symptom described. The multiplication is the trap: 200 app instances × a pool of 20 = 4000 potential connections into a machine that can usefully run maybe a few dozen statements at once. Fixes, in order: **shrink per-instance pools** (total pool size sized from the database's capacity, not from instance count); put a **shared external pooler** in front, multiplexing thousands of client connections onto a small set of server connections; keep transactions short so a pooled connection is returned quickly; and never hold a connection across a network call or user think time.

go deeper

for a junior

Know that connections cost memory, that pools exist to reuse them, and that a transaction should be short and never span an external call.

for a middle

Explain the throughput-versus-concurrency curve and the per-instance × instance-count multiplication, and that a smaller pool can improve latency.

for a senior

Diagnose from wait events and context switches, distinguish queuing from resource saturation, and introduce a shared pooler with a global budget and separate pools for analytic traffic.

for a principal

Treat the pool as admission control for the whole platform: a global budget, load shedding and backoff policy under failure, and the point where connection pressure is really a signal to split the workload or shard.

## Why a connection costs something A database connection is a session with state: authentication and role, current transaction and its snapshot, temporary objects, prepared statements and cached plans, and per-operation working memory for sorts, hashes, and joins. Depending on the engine, it is backed by a dedicated OS process, a dedicated thread, or a slot in a thread pool. Even idle, a connection holds memory. Active, it can allocate several working buffers at once — which is why per-operator memory limits multiplied by high concurrency is a classic route to out-of-memory. So the first limit is simply **memory**: connections × per-connection footprint must leave room for the buffer pool, which is the thing that actually makes the database fast. ## Why more concurrency stops helping The second limit is subtler and is the real answer to the scenario. A machine can only genuinely execute as many statements as it has execution resources: CPU cores, and enough outstanding IO to keep the storage device busy. A common rule of thumb for useful concurrency is on the order of *cores × small factor*, plus an allowance for IO-bound waits. Beyond that point: - **Context switching** rises; time goes to scheduling instead of work. - **CPU cache and TLB thrash** — each switched-in query evicts the previous one's working set. - **Lock queues lengthen.** With 40 sessions contending for a hot row, each waits behind 39 others; the hold time is unchanged but wait time scales with queue depth. Deadlock probability rises superlinearly with concurrency. - **Memory pressure** — many simultaneous sorts and hash tables either spill to disk or squeeze the buffer pool, converting cache hits into IO. The resulting curve is well known: throughput rises with concurrency, plateaus at the resource limit, then **declines** while latency grows without bound. Idle CPU with terrible latency is the signature of queuing on locks or thrashing, not of a machine that needs more cores. That is why "the box looks idle" is not evidence that more connections would help. ## The multiplication trap in a scaled-out app tier This is where scale-up and scale-out interact badly. The application tier scales horizontally — that is the whole point of stateless services — but each instance typically ships with a fixed connection pool (10, 20, 50). Pool size is configured *per instance*, while the constraint lives on the *shared* database. Going from 20 to 200 instances multiplies demanded connections by ten without adding a single database core. The database's admission capacity is a global budget; per-instance configuration silently violates it. The correct mental model: **size the total pool from the database's capacity, then divide by instance count** — not the other way round. A surprisingly small total (a few dozen) often maximizes throughput, because a short queue in the client is far cheaper than contention inside the engine. ## What a pooler adds An external connection pooler sits between clients and the server and multiplexes many client connections onto few server connections: - **Session pooling** — a client keeps a server connection for its whole session; useful mainly to cap totals. - **Transaction pooling** — a server connection is assigned only for the duration of a transaction, then returned. This gives the biggest multiplexing ratio (thousands of clients on tens of server connections) but forbids features that assume session continuity: session-level temporary tables, session variables, advisory locks held across transactions, and cursors that outlive a transaction. Prepared-statement handling needs explicit support. - **Statement pooling** — most aggressive, most restrictive; rarely used. A pooler also gives you one place to enforce a global limit, queue fairly, and shed load — an admission-control point the database itself may not offer. ## Application behaviours that waste connections - Holding a connection open across an external HTTP call, a message send, or user think time. The connection is idle-in-transaction but unavailable — the most common cause of pool exhaustion. - Long-running reporting queries sharing the transactional pool; give them a separate, smaller pool (or a replica) so they cannot starve OLTP traffic. - Opening a transaction at the start of a request handler and committing at the end regardless of whether writes occurred — this pins connections for the whole request duration. - Retry storms: on timeout, every client retries at once, doubling demanded concurrency exactly when the server is least able to serve it. Backoff and a circuit breaker matter here. ## Diagnosing it Look for: connection count near the configured maximum; large numbers of sessions in lock wait; high context-switch rate with unremarkable CPU utilisation; wait events dominated by lock/latch rather than IO; latency that improves when you *reduce* pool size. That last one is the decisive experiment — if halving the pool improves p99, you were over-concurrent, and no hardware purchase would have fixed it. ## Relation to scaling decisions Connection pressure is one of the ways vertical scaling disappoints: you buy a bigger machine, but the workload was never CPU-bound — it was queuing. Conversely, it is one of the pressures that pushes toward scale-out, because sharding divides not only data but also connection demand across independent servers. Deciding which of the two you face is precisely the diagnosis step: idle CPU plus lock waits means fix concurrency; saturated CPU or IO with a healthy pool means you genuinely need more machine, or more machines.

  • How would you pick a pool size?
    Start from the database's usable concurrency — roughly the core count plus an allowance for IO waits — treat that as the global budget, and divide it across application instances rather than configuring per instance. Then measure: increase concurrency until throughput stops improving, and back off, because past the plateau latency grows while throughput does not. Separate pools for long analytic queries and short OLTP work so one cannot starve the other.
  • What breaks when you switch a pooler to transaction pooling mode?
    Anything that assumes the same physical server connection persists between transactions: session-level temporary tables, session variables and settings, advisory locks held across transactions, and cursors that outlive their transaction. Server-side prepared statements need explicit support from the pooler. Applications usually need review before this mode is safe.
  • CPU is at 20% but p99 latency is terrible. What are you looking for?
    Queuing rather than resource exhaustion: sessions waiting on row or page locks, long lock chains behind a hot row, high context-switch rates, or clients blocked waiting for a free pooled connection. Check wait events and the count of sessions in lock wait; if reducing pool size improves p99, the system was over-concurrent and a larger machine would not have helped.

A supermarket with six checkouts: letting 200 shoppers into the queueing area does not check anyone out faster — it just makes every queue longer and the aisles impassable. Six well-fed queues clear the store faster than sixty.

saying these in an interview costs you the question

  • "Raise max_connections" as the first response to connection exhaustion
  • Sizing pools per application instance without a global budget
  • Assuming idle CPU proves the database has spare capacity
  • Believing more concurrency always means more throughput
  • Holding a database transaction open across an external API call

context