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_connectionsReserve at least superuser_reserved_connections + room for monitoring (5–10 is reasonable).
Multiple pooler replicas
replicas × MAX_CONNECTIONS + reserved ≤ PostgreSQL max_connectionsEach 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_connectionsWhen 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.
| Workload | Reasonable starting MAX_CONNECTIONS | Adjust upward when |
|---|---|---|
| Small API or internal tool | 10–25 | Wait time on connection acquire becomes visible |
| Mid-traffic web app or service | 25–50 | Pool sits at >80% utilization at peak |
| High-traffic OLTP behind transaction pooling | 50–100 per replica | PG 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.
| Workload | Effect of adding pgagroal | Why |
|---|---|---|
| Many short-lived clients (serverless, lambdas) | Large win | Removes per-request connection setup against PostgreSQL |
| High-concurrency OLTP, short transactions | Large win (transaction mode) | Decouples client count from backend count |
| Long-lived application workers with persistent connections | Small win or neutral | Workers already amortize connection cost |
| Long transactions or batch jobs | Neutral | Backend is held for the whole job; pool reuse never triggers |
| Heavy single-statement queries (analytics) | Neutral or slight loss | PostgreSQL is the bottleneck; pooler adds a hop |
| Tiny database, single client | Slight loss | Extra 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.