skills/pgbouncer-architect/SKILL.md
Designs and configures PgBouncer connection pooling for PostgreSQL based on actual workload analysis. Calculates optimal pool sizes using server capacity formulas, selects the correct pooling mode (session/transaction/statement) based on ORM compatibility, generates production-ready pgbouncer.ini and docker-compose.yml, audits active CVEs, and validates ORM-specific constraints. Trigger when: pgbouncer setup, connection pooling, too many connections, max_connections exceeded, connection pool sizing, pgbouncer config, database connections scaling, pool mode selection, or any mention of PgBouncer in a PostgreSQL context. Part of database-ecosystem.
npx skillsauth add fatih-developer/fth-skills pgbouncer-architectInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Analyze the workload, calculate the right numbers, select the right mode, generate production-ready config. Never hardcode values — always derive them from actual system parameters.
Core principle: Wrong pool mode = silent bugs. Wrong pool size = either wasted resources or connection starvation. Always ask before generating.
Before writing a single config line, collect these inputs. Ask the user or derive from context:
□ PostgreSQL max_connections (default: 100)
□ Server RAM available to PostgreSQL
□ Number of CPU cores
□ Current active connections (if existing system):
SELECT count(*) FROM pg_stat_activity;
□ Application framework / language
(Node.js, Python/FastAPI, Go, .NET, ...)
□ ORM / driver:
Drizzle · Prisma · TypeORM · SQLAlchemy · asyncpg · pg · pgx · Npgsql
□ Number of application instances / workers
□ Expected concurrent users (peak)
□ Average transaction duration (ms)
□ Do you use any of these? (critical for mode selection)
- Prepared statements (persisted across transactions)
- LISTEN / NOTIFY
- Advisory locks
- SET / RESET session variables
- Temporary tables
- WITH HOLD cursors
□ Deployment: Docker Compose · Kubernetes · bare metal · managed cloud
□ Is PgBouncer alongside PostgreSQL or on a separate host?
□ TLS required between app ↔ PgBouncer ↔ PostgreSQL?
□ High availability required? (multiple PgBouncer instances)
□ Monitoring stack: Prometheus · Grafana · Datadog · none
📋 System Profile
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PostgreSQL max_connections : N
Application workers : N
ORM / driver : X
Session features used : yes/no
Peak concurrent users : N
Deployment : X
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Proceed with these values?
This is the most critical decision. Wrong mode breaks features silently.
| Feature Used | Session | Transaction | Statement | |-------------|---------|-------------|-----------| | Prepared statements (persistent) | ✅ | ❌ | ❌ | | LISTEN / NOTIFY | ✅ | ❌ | ❌ | | Advisory locks | ✅ | ❌ | ❌ | | SET session variables | ✅ | ❌ | ❌ | | Temporary tables | ✅ | ❌ | ❌ | | WITH HOLD cursors | ✅ | ❌ | ❌ | | Connection efficiency | 🔴 Low | 🟢 High | 🟢 Highest | | Scalability | 🔴 Low | 🟢 High | 🟢 Highest |
IF any session-level feature is used:
→ SESSION mode
→ Warn: connection efficiency is low, consider refactoring
ELSE IF ORM is Prisma or Drizzle:
→ TRANSACTION mode
→ Note: disable prepared statement cache (see ORM section)
ELSE IF ORM is SQLAlchemy + asyncpg:
→ TRANSACTION mode
→ Note: set statement_cache_size=0
ELSE IF all queries are simple, stateless:
→ TRANSACTION mode (default recommendation)
NEVER recommend STATEMENT mode unless:
→ User explicitly requests it
→ AND workload is confirmed single-statement only
🔧 Selected Mode: TRANSACTION
Reason: Drizzle ORM detected, no session-level features reported.
Trade-off: Prepared statements will not persist across transactions.
Action required: See ORM compatibility section.
Never use arbitrary numbers. Derive from system parameters.
# PostgreSQL connections reserved for PgBouncer
pgbouncer_reserved = max_connections - superuser_reserved - admin_connections
# superuser_reserved default: 3
# admin_connections: 2-5 (for maintenance, monitoring)
pgbouncer_reserved = max_connections - 3 - 3 # conservative
# default_pool_size per database-user pair
default_pool_size = floor(pgbouncer_reserved / number_of_pools)
# For single app, single DB (most common case):
default_pool_size = pgbouncer_reserved # e.g. 94 for max_connections=100
# Rule of thumb: 3-5x the number of actual server connections
max_client_conn = default_pool_size * 4
# But also consider:
max_client_conn >= app_workers * connections_per_worker * 1.5 # headroom
# Take the larger of the two:
max_client_conn = max(default_pool_size * 4, app_workers * conn_per_worker * 1.5)
reserve_pool_size = ceil(default_pool_size * 0.1) # 10% of pool
reserve_pool_timeout = 3 # seconds before using reserve
Given:
max_connections = 100
app_workers = 8
pools = 1 (single app, single DB)
Calculation:
pgbouncer_reserved = 100 - 3 - 3 = 94
default_pool_size = 94
reserve_pool_size = ceil(94 * 0.1) = 10
max_client_conn = max(94 * 4, 8 * 10 * 1.5) = max(376, 120) = 376
Result:
default_pool_size = 94
reserve_pool_size = 10
max_client_conn = 376
# How long an unused server connection stays open
server_lifetime = 3600 # 1 hour (reduce for cloud DBs with idle limits)
# How long an idle server connection stays before disconnect
server_idle_timeout = 600 # 10 minutes
# How long a client can sit idle
client_idle_timeout = 300 # 5 minutes
# How long to wait for a server connection before error
query_wait_timeout = 30 # 30 seconds
# For cloud databases (RDS, Supabase, Neon) — reduce:
server_lifetime = 300 # 5 min (avoid cloud idle disconnects)
server_idle_timeout = 120 # 2 min
Each ORM has specific requirements. Apply automatically based on Phase 1 detection.
// In transaction mode: disable prepared statement caching
// drizzle config:
const db = drizzle(pool, {
// Drizzle uses node-postgres (pg) under the hood
// pg handles prepared statements per connection
// In transaction mode, connections change per transaction
// → Use query strings, not prepared statement names
});
// Connection string: point to PgBouncer port
const pool = new Pool({
connectionString: process.env.DATABASE_URL, // pgbouncer:6432
// Do NOT set statement_timeout here — set in pgbouncer.ini
});
⚠️ Warning: Drizzle's db.transaction() works fine in transaction mode.
Advisory locks inside transactions do NOT work — use session mode if needed.
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // pgbouncer:6432
// Add pgbouncer=true to connection string:
// postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true
}
// .env
DATABASE_URL="postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true&connection_limit=1"
// ↑ disables Prisma's own pool
// ↑ 1 conn per instance
⚠️ Warning: pgbouncer=true disables Prisma's prepared statements. This is required.
const dataSource = new DataSource({
type: "postgres",
host: "pgbouncer", // ← PgBouncer host
port: 6432, // ← PgBouncer port
poolSize: 1, // ← Let PgBouncer handle pooling
extra: {
statement_timeout: 30000,
// Disable TypeORM's prepared statements in transaction mode:
prepare: false,
}
});
engine = create_async_engine(
DATABASE_URL, # postgresql+asyncpg://user:pass@pgbouncer:6432/db
pool_size=1, # Let PgBouncer pool
max_overflow=0,
connect_args={
"statement_cache_size": 0, # ← REQUIRED for transaction mode
"prepared_statement_cache_size": 0,
}
)
const pool = new Pool({
connectionString: process.env.DATABASE_URL, // pgbouncer:6432
max: 1, // Let PgBouncer pool
// No special config needed for transaction mode
// But: cannot use pool.connect() + persistent prepared statements
});
[databases]
; Database alias = actual PostgreSQL connection
; Use * to allow any database name to pass through
{DB_NAME} = host={PG_HOST} port={PG_PORT} dbname={PG_DBNAME}
; Read replica (optional)
; {DB_NAME}_readonly = host={REPLICA_HOST} port=5432 dbname={PG_DBNAME}
[pgbouncer]
;; Network
listen_addr = 0.0.0.0
listen_port = 6432
;; Auth — NEVER use trust or plain in production
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
;; Pooling
pool_mode = {CALCULATED_MODE} ; session | transaction | statement
max_client_conn = {CALCULATED} ; derived from formula
default_pool_size = {CALCULATED} ; derived from formula
min_pool_size = {CEIL(POOL*0.1)} ; warm pool — avoid cold start
reserve_pool_size = {CALCULATED} ; 10% of default_pool_size
reserve_pool_timeout = 3
;; Timeouts
server_lifetime = {CALCULATED} ; 3600 standard / 300 cloud
server_idle_timeout = {CALCULATED} ; 600 standard / 120 cloud
client_idle_timeout = 300
query_wait_timeout = 30
client_login_timeout = 10
;; TLS (enable in production)
; server_tls_sslmode = require
; server_tls_ca_file = /etc/ssl/certs/ca-certificates.crt
; client_tls_sslmode = require
; client_tls_cert_file = /etc/pgbouncer/pgbouncer.crt
; client_tls_key_file = /etc/pgbouncer/pgbouncer.key
;; Logging
log_connections = 0 ; set to 1 only for debugging (high volume)
log_disconnections = 0
log_pooler_errors = 1
stats_period = 60
;; Admin console — for monitoring queries
admin_users = pgbouncer_admin
stats_users = pgbouncer_stats
;; Ignore startup params sent by ORMs
; Required for some ORMs that send extra connection params
ignore_startup_parameters = extra_float_digits,options
; Generate hashes with:
; psql -c "SELECT concat('\"', usename, '\" \"', passwd, '\"') FROM pg_shadow WHERE usename = '{YOUR_USER}';"
;
; NEVER put plaintext passwords here.
; Format: "username" "SCRAM-SHA-256$<iterations>:<salt>$<stored_key>:<server_key>"
"{APP_USER}" "SCRAM-SHA-256$4096:{HASH_FROM_PSQL}"
"pgbouncer_admin" "SCRAM-SHA-256$4096:{HASH_FROM_PSQL}"
services:
pgbouncer:
image: bitnami/pgbouncer:latest # actively maintained, rootless
# Alternative: edoburu/pgbouncer (also good)
restart: unless-stopped
ports:
- "6432:6432"
environment:
# Bitnami image env vars
POSTGRESQL_HOST: ${POSTGRESQL_HOST:-postgres}
POSTGRESQL_PORT: ${POSTGRESQL_PORT:-5432}
POSTGRESQL_DATABASE: ${POSTGRESQL_DATABASE}
POSTGRESQL_USERNAME: ${POSTGRESQL_USERNAME}
POSTGRESQL_PASSWORD: ${POSTGRESQL_PASSWORD}
PGBOUNCER_POOL_MODE: ${PGBOUNCER_POOL_MODE:-transaction}
PGBOUNCER_MAX_CLIENT_CONN: ${PGBOUNCER_MAX_CLIENT_CONN:-400}
PGBOUNCER_DEFAULT_POOL_SIZE: ${PGBOUNCER_DEFAULT_POOL_SIZE:-94}
PGBOUNCER_MIN_POOL_SIZE: ${PGBOUNCER_MIN_POOL_SIZE:-10}
PGBOUNCER_RESERVE_POOL_SIZE: ${PGBOUNCER_RESERVE_POOL_SIZE:-10}
PGBOUNCER_IGNORE_STARTUP_PARAMETERS: extra_float_digits,options
PGBOUNCER_AUTH_TYPE: scram-sha-256
volumes:
- ./pgbouncer.ini:/bitnami/pgbouncer/conf/pgbouncer.ini:ro
- ./userlist.txt:/bitnami/pgbouncer/conf/userlist.txt:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 6432 -U ${POSTGRESQL_USERNAME}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
depends_on:
postgres:
condition: service_healthy
networks:
- db_network
networks:
db_network:
driver: bridge
Provide these as a ready-to-use reference:
-- Connect to PgBouncer admin console:
-- psql -h 127.0.0.1 -p 6432 -U pgbouncer_admin pgbouncer
-- Pool status: active vs idle vs waiting
SHOW POOLS;
-- Key columns: cl_active, cl_waiting, sv_active, sv_idle, sv_used, maxwait
-- If maxwait > 0: clients are waiting → pool may be undersized
-- Aggregate stats (requests/sec, avg query time)
SHOW STATS;
-- Current client connections
SHOW CLIENTS;
-- Current server (PostgreSQL) connections
SHOW SERVERS;
-- Global config
SHOW CONFIG;
-- Live update pool size without restart:
SET default_pool_size=120;
RELOAD;
-- Prometheus metrics (if pgbouncer_exporter is deployed):
-- Scrape: http://pgbouncer-exporter:9127/metrics
-- Key metrics:
-- pgbouncer_pools_cl_waiting → should be 0
-- pgbouncer_pools_sv_active → vs pool_size
-- pgbouncer_stats_avg_wait_time → should be <5ms
Always check before finalizing config.
CVE-2025-12819 (PgBouncer < 1.25.0) — CRITICAL
Affected: auth_user + track_extra_parameters + auth_query combination
Risk: Unauthenticated SQL execution
Fix: Upgrade to >= 1.25.1
CVE-2024-XXXX — Check https://www.pgbouncer.org/changelog.html for latest
Always pin to a specific version:
# BAD — gets whatever latest is, may be vulnerable
image: edoburu/pgbouncer:latest
# GOOD — pinned, predictable
image: bitnami/pgbouncer:1.25.1
□ auth_type = scram-sha-256 (never md5, never trust in production)
□ No plaintext passwords in userlist.txt
□ admin_users locked down (not exposed to app)
□ TLS enabled between app ↔ PgBouncer AND PgBouncer ↔ PostgreSQL
□ PgBouncer admin port (6432/pgbouncer db) not exposed publicly
□ Image pinned to specific version
□ userlist.txt mounted read-only (:ro)
□ No DATABASE_URL containing passwords in docker-compose env (use secrets)
Generate pgbouncer-report.md summarizing all decisions:
# PgBouncer Architecture Report
**Generated:** {date}
**Version:** pgbouncer-architect v1.0.0
## System Profile
| Parameter | Value |
|-----------|-------|
| PostgreSQL max_connections | N |
| App workers | N |
| ORM | X |
| Session features | yes/no |
## Decisions
### Pool Mode: {MODE}
**Reason:** {why this mode}
**Trade-offs:** {what this mode cannot do}
### Pool Sizing
| Parameter | Value | Formula |
|-----------|-------|---------|
| default_pool_size | N | max_connections - 6 |
| max_client_conn | N | pool_size × 4 |
| reserve_pool_size | N | pool_size × 10% |
## ORM Configuration Required
{ORM-specific changes the developer must make}
## Security
{CVE status, checklist results}
## Monitoring
{Where to look, what to watch}
## Files Generated
- pgbouncer.ini
- docker-compose-pgbouncer.yml
- userlist.txt (template — fill hashes manually)
references/pooling-modes.md — Deep dive: session vs transaction vs statementreferences/sizing-formulas.md — Extended formulas for complex multi-tenant setupstools
Create, optimize, critique, and structure prompts for AI systems. Use this skill whenever the user is designing or improving a prompt, system prompt, coding prompt, image prompt, evaluation rubric, agent prompt, workflow prompt, or MCP-oriented prompt package. Also use it when the user asks to turn vague AI behavior into a precise instruction set, tool policy, agent spec, or prompt architecture.
testing
Assumption-first architecture review skill to stress-test project plans and expose hidden risks.
testing
Enforce and manage DESIGN.md specifications, extract design systems from URLs, and combine design reasoning with token roles to prevent drift.
testing
Forces the agent to act with a Claude-like product mindset, prioritizing user journey, UX states, and visual quality before coding.