Skip to main content

Performance and capacity

Sizing the pool, the latency budget, and where pgagroal does and does not help.

What pooling actually costs

pgagroal adds one network hop and a small amount of byte shuffling on each query. Statements are not parsed, plans are not cached, results are not transformed. The per-statement overhead in session mode is dominated by the extra hop, not by anything pgagroal does in user space.

In transaction mode there is an additional cost: at the start of every transaction, pgagroal must acquire a backend from the pool and detach it on commit. This is fast (microseconds) when the pool has idle connections and slow (waiting on blocking_timeout) when it does not.

If a pooler appears to be slow, the cause is almost always the network hop or pool exhaustion, not pgagroal itself. See Troubleshooting for how to isolate the two.

Capacity math

The constraint is PostgreSQL's max_connections. Every backend pgagroal opens counts against it, alongside admin sessions, monitoring tools, replication slots, and anything else that connects directly.

Single pooler, single database

MAX_CONNECTIONS  +  reserved  ≤  PostgreSQL max_connections

Reserve at least superuser_reserved_connections + room for monitoring (5–10 is reasonable).

Multiple pooler replicas

replicas × MAX_CONNECTIONS  +  reserved  ≤  PostgreSQL max_connections

Each replica opens its own pool. Two replicas with MAX_CONNECTIONS=50 consume up to 100 backend connections.

Multiple databases through one pooler

Σ (per-database max_size)  +  reserved  ≤  PostgreSQL max_connections

When pgagroal is configured with per-database pools, the sum of all max_size values is the budget, not any single one.

Plan to a number, not to a default. PostgreSQL's max_connections default of 100 is a starting point, not a target. Choose it together with the pool size based on what the host can actually serve.

Pool sizing: starting points

The right pool size is a function of how many transactions the application runs in parallel, not how many clients it has. A useful starting heuristic:

needed_pool_size  ≈  peak_concurrent_transactions

peak_concurrent_transactions  ≈  throughput (txn/s)  ×  avg_txn_duration (s)

For example: 2,000 transactions per second at 5ms each implies ~10 concurrent transactions, so a pool of 20–25 gives generous headroom. Most workloads need a much smaller pool than they think.

WorkloadReasonable starting MAX_CONNECTIONSAdjust upward when
Small API or internal tool10–25Wait time on connection acquire becomes visible
Mid-traffic web app or service25–50Pool sits at >80% utilization at peak
High-traffic OLTP behind transaction pooling50–100 per replicaPG CPU and IO have headroom but pool is saturated

Larger pools are not better. Above the point where PostgreSQL itself becomes the bottleneck (CPU, IO, lock contention), more backend connections make throughput worse, not better.

Where added latency comes from

When pgagroal makes a workload slower, the cause is usually one of four things. In rough order of magnitude:

Network distance from app to pooler

A pooler in a different availability zone than the application doubles the round-trip latency for every statement. Co-locate the pooler with the application whenever possible: same node as a sidecar, or at least the same subnet. The PostgreSQL hop pays the same penalty either way.

Pool exhaustion under transaction pooling

When all backends are busy, new transactions wait up to blocking_timeout. A pool sized for average load melts under bursts. Either size for the burst or accept that bursts will queue.

Validation overhead

Foreground validation (validation = foreground) adds a round-trip per acquire. On a low-latency network it is negligible; on a high-latency one it is the largest cost on the path. Background validation amortizes this.

Slow queries that were always slow

Pooling does not change query plans, indexes, or PostgreSQL-side cost. If a query was slow direct, it is slow through the pooler. Compare query latency direct vs through the pooler before blaming pgagroal.

When pgagroal helps and when it does not

The benefit of a pooler is not uniform across workloads. Some get a multiple, some get nothing, some get worse.

WorkloadEffect of adding pgagroalWhy
Many short-lived clients (serverless, lambdas)Large winRemoves per-request connection setup against PostgreSQL
High-concurrency OLTP, short transactionsLarge win (transaction mode)Decouples client count from backend count
Long-lived application workers with persistent connectionsSmall win or neutralWorkers already amortize connection cost
Long transactions or batch jobsNeutralBackend is held for the whole job; pool reuse never triggers
Heavy single-statement queries (analytics)Neutral or slight lossPostgreSQL is the bottleneck; pooler adds a hop
Tiny database, single clientSlight lossExtra hop with no concurrency benefit

See Choosing your connection path for measured per-statement latency in each scenario.

Common mis-tunings

Pool sized to client count

The point of a pooler is that backend count does not need to match client count. Sizing to clients defeats the model and pushes load onto PostgreSQL for no benefit.

Sum of pools exceeds max_connections

Two pooler replicas with MAX_CONNECTIONS=100 against a PostgreSQL with max_connections=100will reach "too many connections" with one pooler alone using its full pool. Always do the math across replicas.

Transaction pipeline for a session-scoped workload

If the application uses long-lived connections and a modest client count, transaction pooling adds acquire overhead without changing the connection ratio. Use session instead. See Concepts.

Validation off when the backend restarts often

If PostgreSQL restarts independently (rolling upgrades, failover, RDS maintenance), validation = off means pgagroal will hand out a stale connection on the next acquire. Use background validation to test the pool on a timer with no per-acquire cost.

See also: Concepts for pipeline tradeoffs, Architecture for how the pool actually works, Observability for what to watch in production, or Troubleshooting for what to do when sizing is wrong.

Run pgagroal

docker pull elevarq/pgagroal:1.0.0