Pooling Basics
Each PostgreSQL backend is a process with memory overhead. Raising max_connections without pooling increases context switching and cache churn; PgBouncer multiplexes clients to fewer server backends on PostgreSQL 18.4.
Recipe
Quick-reference recipe card - copy-paste ready.
SHOW max_connections;
SELECT count(*) AS active_backends FROM pg_stat_activity WHERE backend_type = 'client backend';
-- sizing heuristic (OLTP): pool_size ~= (CPU cores * 2) + effective_spindle_count
-- validate with p95 latency, not app instance countWhen to reach for this: Connection count approaches max_connections, or CPU idle while queries queue in pool wait.
Working Example
SELECT
setting::int AS max_connections,
(SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend') AS current,
setting::int - (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend') AS headroom
FROM pg_settings
WHERE name = 'max_connections';
SELECT state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state
ORDER BY count DESC;Pair with PgBouncer SHOW POOLS (admin console) to see cl_active, cl_waiting, and sv_active.
What this demonstrates:
- Server connection headroom vs app connections
- Idle vs active backend breakdown
- Why pool wait time matters more than raw connection count
Deep Dive
How It Works
- App servers open many TCP connections; pooler reuses fewer PostgreSQL backends.
- Each backend allocates
work_mempotential per query and private caches. - Context switching rises when hundreds of backends compete for CPU.
- Pool sits between apps and Postgres: apps -> PgBouncer -> PostgreSQL.
Sizing Signals
| Metric | Healthy | Investigate |
|---|---|---|
cl_waiting in PgBouncer | Near 0 | Sustained > 0 |
| CPU utilization | Moderate under load | Low CPU, high wait |
max_connections usage | < 70% peak | > 85% peak |
SQL Notes
SELECT usename, application_name, state, wait_event_type, query_start
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY query_start NULLS LAST
LIMIT 20;Gotchas
- max_connections = app instances * pool size - Exhausts RAM and CPU. Fix: Central pooler; size server pool from cores.
- No pooler for serverless burst - Each lambda opens connections. Fix: External pool (RDS Proxy, PgBouncer, Supavisor).
- Ignoring idle in transaction - Holds server connection from pool. Fix:
idle_in_transaction_session_timeout. - One giant pool for batch and OLTP - BI scans starve short queries. Fix: Separate pools and roles.
- Raising max_connections instead of pooling - Linear memory growth. Fix: Pool first, scale connections modestly.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| PgBouncer | General OLTP multiplexing | Need session-scoped temp tables everywhere |
| Odyssey / pgpool | Vendor-specific features | Simple PgBouncer suffices |
| RDS Proxy | Managed AWS fleets | Self-managed K8s with PgBouncer |
FAQs
How many server connections per CPU core?
Often 2-4 for OLTP start; measure latency under load.
Does pooling reduce query parallelism?
No, but too few backends cap concurrent queries; balance wait vs CPU.
Superuser connections?
Reserve superuser slots; exclude admin from app pools.
TLS termination?
Pooler or app can terminate TLS; align with compliance.
Connection storms on deploy?
Stagger pod starts; pooler absorbs burst better than raw Postgres.
pg_stat_activity vs pool stats?
Server shows backends; pooler shows client wait queue.
read replicas pooling?
Separate pool per target host/route.
JDBC pool vs PgBouncer?
Use both: app pool modest, central PgBouncer for server protection.
max_connections formula?
Start conservative; increase only with memory model proof.
Next?
PgBouncer modes: session vs transaction pooling.
Related
- PgBouncer Modes - Pooling semantics
- Pooling Best Practices - Sizing checklist
- Connection Math - Capacity formulas
- Role & Timeout per Pool - Workload separation
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.