Skip to content
Back to skills

Connection Pool Tuning

ASecurity

Playbook for sizing and configuring a database connection pool (PgBouncer or application-level) — calculating the right pool size, choosing the pooling mode, diagnosing pool exhaustion, and avoiding the common over-provisioning trap.

  • 7 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 23, 2026
ai-agentspythonsqlawsdebuggingdatabasebackend

Works with

  • cli

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add mcorbett51090/RavenClaude --skill connection-pool-tuning --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Connection Pool Tuning?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Connection Pool Tuning
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mcorbett51090-connection-pool-tuning/badge)](https://www.skillsdirectory.com/skills/mcorbett51090-connection-pool-tuning)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: connection-pool-tuning
description: "Playbook for sizing and configuring a database connection pool (PgBouncer or application-level) — calculating the right pool size, choosing the pooling mode, diagnosing pool exhaustion, and avoiding the common over-provisioning trap."
---

# Connection Pool Tuning

## When to invoke

Use when: connections are exhausted under load (`too many connections`, `connection refused`), p99 latency spikes without corresponding query slowness, you are scaling horizontally and haven't revisited the pool, or you are adding a new service that shares a database.

## Concepts

| Term | Meaning |
|---|---|
| `max_connections` | Postgres hard limit (server-side); each connection holds ~5–10 MB of server RAM |
| Pool size | Maximum connections the pool will open to the backend |
| Min idle | Connections held warm; trade-off between latency and resource |
| Checkout timeout | How long a caller waits before getting "pool exhausted" error |
| PgBouncer | External pooler — sits between app and Postgres; recommended for high-concurrency |

## Step 1 — Establish the capacity envelope

```sql
-- Current headroom
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity WHERE state != 'idle';
SELECT count(*) FROM pg_stat_activity;
```

Reserve connections:
- `~3` for superuser (`superuser_reserved_connections`)
- `~2–5` for monitoring/admin tools
- Remaining: available to pool(s)

**Formula:** `available = max_connections - reserved`

For RDS/Aurora: `max_connections` is instance-class-based. `db.t3.medium` allows ~170; `db.r6g.large` allows ~1500. Check AWS docs for exact values — do not assume.

## Step 2 — Choose pooling mode (PgBouncer)

| Mode | When to use | Caveats |
|---|---|---|
| Session | Each client gets a dedicated backend connection for its lifetime | No multiplexing benefit; avoid for web apps |
| Transaction | Connection released back to pool at transaction end | **Recommended for most apps.** Breaks `SET` / prepared statements without `server_reset_query` |
| Statement | Connection released after each statement | Incompatible with multi-statement transactions |

**Default recommendation: transaction pooling.**

Incompatibilities to resolve in transaction mode:
- Disable `DISCARD ALL` and use `server_reset_query = DISCARD ALL` in PgBouncer
- Prepared statements: use named prepared statements per-session, or disable server-side prepared statements in the app driver

## Step 3 — Size the pool

```
Pool size = (# app instances) × (# threads/workers per instance) × (avg concurrent DB calls per thread)
```

Example: 10 app pods, 4 workers each, each worker holds 1 DB connection at a time → **40 connections needed**.

Rule of thumb: pool size per service rarely exceeds `(CPU cores on DB server) × 2 + num_disks`. More connections than this causes context-switch overhead and slows all queries.

**PgBouncer `pgbouncer.ini` example:**

```ini
[databases]
mydb = host=db.internal port=5432 dbname=mydb

[pgbouncer]
pool_mode = transaction
max_client_conn = 500        # app-side: total PgBouncer clients
default_pool_size = 40       # backend connections to Postgres per database+user pair
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
log_connections = 0          # set to 1 only during debugging
```

## Step 4 — Application-level pool (HikariCP / SQLAlchemy / pgx)

If using an app-level pool (no external pooler):

```python
# SQLAlchemy example
engine = create_engine(
    DATABASE_URL,
    pool_size=10,          # per process
    max_overflow=5,        # burst above pool_size; total max = 15
    pool_timeout=30,       # raise if no connection available after 30s
    pool_pre_ping=True,    # test connection health on checkout
    pool_recycle=3600,     # recycle connections older than 1 hour
)
```

`pool_pre_ping=True` is critical in containerized environments where TCP connections are silently dropped after idle periods.

## Step 5 — Diagnose pool exhaustion

```sql
-- Connection wait times and idle transactions
SELECT pid, state, wait_event_type, wait_event, query_start, now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;

-- Connections by application
SELECT application_name, state, count(*)
FROM pg_stat_activity
GROUP BY 1, 2 ORDER BY 3 DESC;
```

Check `pg_stat_activity` for:
- **`idle in transaction`** rows with long duration → application not committing; fix the ORM/transaction boundary.
- **`ClientRead` or `ClientActive`** exceeding pool size → pool is full and callers are queueing.

## Pitfalls

- **Over-sizing the pool** — 500 backend connections on a 4-core database causes lock contention and scheduler overhead that slows every query.
- **`pool_pre_ping=False` on long-lived pods** — stale TCP connections fail silently under load, causing cryptic "connection closed" errors.
- **Transaction pooling + `SET` commands** — settings like `SET search_path` are per-session; in transaction pooling they are lost at transaction end. Use `search_path` in the connection string instead.
- **Ignoring `idle in transaction`** — these hold locks and bloat. A 30-second idle-in-transaction limit (`idle_in_transaction_session_timeout = 30s`) is a safe guard.
- **Not accounting for migrations** — migration tools open their own connections outside the pool; subtract their concurrency from available headroom before sizing.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…