git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/db-connection-pooling" ~/.claude/skills/intense-visions-harness-engineering-db-connection-pooling && rm -rf "$T"
agents/skills/claude-code/db-connection-pooling/SKILL.mdConnection Pooling
External connection poolers like PgBouncer sit between the application and database, multiplexing many application connections onto fewer database connections to prevent connection exhaustion.
When to Use
- Application has more processes/threads than the database can handle with direct connections
- Seeing
errors in PostgreSQL logsFATAL: too many connections - Running a microservices architecture where each service opens its own connection pool
- Deploying to serverless environments where connection reuse is difficult
- Tuning PgBouncer or a similar pooler for a production workload
Instructions
Key Concepts
1. Why Pool Externally
PostgreSQL forks a backend process for each connection. Each process consumes 5-10 MB of memory. At 500 connections, that is 2.5-5 GB of memory just for connection overhead, plus contention on shared resources like
ProcArrayLock. An external pooler holds a small number of database connections (20-100) and multiplexes thousands of application connections onto them.
2. PgBouncer Pool Modes
; pgbouncer.ini [databases] mydb = host=127.0.0.1 port=5432 dbname=mydb [pgbouncer] listen_port = 6432 pool_mode = transaction ; session | transaction | statement max_client_conn = 1000 ; max application connections to PgBouncer default_pool_size = 25 ; database connections per user/database pair reserve_pool_size = 5 ; extra connections for burst traffic reserve_pool_timeout = 3 ; seconds before reserve pool activates
Session mode: A database connection is assigned when the client connects and released when the client disconnects. Safest but least efficient -- almost no multiplexing benefit.
Transaction mode: A database connection is assigned at
BEGIN and released at COMMIT/ROLLBACK. Best balance of safety and efficiency. Most production deployments use this mode.
Statement mode: A database connection is assigned per statement. Only works for simple, autocommit queries. Breaks multi-statement transactions.
3. Transaction Mode Limitations
In transaction mode, session-level state is lost between transactions:
-- These features BREAK in transaction mode: SET search_path = 'tenant_1'; -- lost after transaction ends PREPARE my_stmt AS SELECT ...; -- lost after transaction ends LISTEN channel_name; -- lost after transaction ends CREATE TEMP TABLE tmp (...); -- lost after transaction ends -- These features WORK in transaction mode: BEGIN; ... COMMIT; -- transactions work normally SET LOCAL search_path = '...'; -- scoped to transaction, OK
4. Pool Sizing Formula
default_pool_size = max_database_connections / number_of_pooler_instances Example: PostgreSQL max_connections = 100 Reserved for superuser/monitoring = 10 Available for application = 90 PgBouncer instances = 2 default_pool_size = 90 / 2 = 45 per instance
The
max_client_conn can be much larger because PgBouncer queues requests when all database connections are busy.
Worked Example
A SaaS platform runs 12 microservices, each maintaining a connection pool of 20 connections. That is 240 direct connections, but the PostgreSQL instance only supports 100.
; pgbouncer.ini [databases] saasdb = host=db.internal port=5432 dbname=saasdb [pgbouncer] listen_port = 6432 pool_mode = transaction max_client_conn = 500 default_pool_size = 40 reserve_pool_size = 10 reserve_pool_timeout = 3 server_idle_timeout = 300 server_lifetime = 3600 log_connections = 0 log_disconnections = 0
Each microservice connects to PgBouncer on port 6432 instead of PostgreSQL on port 5432. PgBouncer maintains 40 real database connections and queues excess requests. The 12 services can open up to 500 connections to PgBouncer combined, but only 40-50 database connections are active at any time.
Monitor with the PgBouncer admin console:
-- Connect to PgBouncer admin psql -p 6432 -U pgbouncer pgbouncer -- Check pool status SHOW POOLS; -- cl_active: application connections actively using a database connection -- cl_waiting: application connections waiting for a database connection -- sv_active: database connections processing a query -- sv_idle: database connections idle and available -- Check connection counts SHOW STATS;
Anti-Patterns
-
Setting
equal todefault_pool_size
. This defeats the purpose of pooling. The pool size should be a fraction of max_connections, leaving room for superuser connections, monitoring, and burst capacity.max_connections -
Using session mode with short-lived connections. Session mode holds a database connection for the entire client session. If clients connect, run one query, and disconnect, session mode provides zero multiplexing benefit and adds latency from PgBouncer's overhead.
-
Using
statements in transaction mode. Prepared statements are session-level state. In transaction mode, the next transaction may get a different backend connection where the prepared statement does not exist, causingPREPARE
.ERROR: prepared statement does not exist -
Running PgBouncer and the application on the same host without connection limits. If the application opens more connections than PgBouncer can queue, both PgBouncer and the application compete for the same CPU and memory. Run PgBouncer on a dedicated host or set strict
limits.max_client_conn -
Not monitoring
. Ifcl_waiting
is consistently above zero, application connections are waiting for database connections. Either increasecl_waiting
(if the database can handle it) or optimize query performance to release connections faster.default_pool_size
PostgreSQL Specifics
- PostgreSQL 14+ built-in connection pooling is limited to
for parallel query workers; it does not pool client connections. External pooling is still required.connection_pool_size
provides PgBouncer stats as PostgreSQL foreign tables for integration with monitoring dashboards.pgbouncer_fdw- Supabase Vibes uses PgBouncer in transaction mode by default for serverless deployments.
- AWS RDS Proxy and Google Cloud SQL Auth Proxy provide managed pooling with IAM authentication.
Details
Advanced Topics
Application-Level vs External Pooling: Most application frameworks include a built-in connection pool (e.g., HikariCP for Java, database/sql for Go). These manage connections within a single process. External poolers like PgBouncer manage connections across all processes. Use both: the application pool manages per-process concurrency, and PgBouncer manages database-level concurrency.
Multi-Database Pooling:
[databases] db1 = host=primary.db port=5432 dbname=db1 db2 = host=primary.db port=5432 dbname=db2 db1_ro = host=replica.db port=5432 dbname=db1
Route read queries to replicas by connecting to
db1_ro instead of db1.
PgBouncer Authentication:
auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt ; Or delegate to PostgreSQL: auth_type = hba auth_hba_file = /etc/pgbouncer/pg_hba.conf
Engine Differences
MySQL's connection model differs fundamentally: MySQL uses one thread per connection rather than one process per connection. Threads are lighter-weight (typically 256KB-1MB stack per thread vs 5-10MB per PostgreSQL process). MySQL handles more concurrent connections natively, but thread contention becomes a bottleneck beyond a few thousand connections.
ProxySQL serves a similar role to PgBouncer for MySQL. It supports query routing, connection multiplexing, and read-write splitting. ProxySQL uses a regex-based query routing system that is more feature-rich than PgBouncer but more complex to configure.
Real-World Case Studies
A ride-sharing platform ran 200 microservices, each with a pool of 10 connections, totaling 2,000 direct database connections. PostgreSQL was spending more time managing connections than processing queries. Deploying PgBouncer in transaction mode with
default_pool_size = 80 and max_client_conn = 5000 reduced actual database connections from 2,000 to 80. Query latency at p99 dropped from 450ms to 120ms because PostgreSQL's shared buffer management was no longer thrashing. The cl_waiting metric showed occasional queuing during peak hours, resolved by increasing default_pool_size to 100.
Source
Process
- Deploy PgBouncer between the application and PostgreSQL; start with
.pool_mode = transaction - Set
todefault_pool_size
.(max_connections - reserved) / pooler_instances - Update application connection strings to point to PgBouncer's port.
- Monitor
forSHOW POOLS
andcl_waiting
to tune pool sizing.sv_idle
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: db-connection-sizing, db-acid-in-practice, db-isolation-levels
Success Criteria
- An external connection pooler (PgBouncer or equivalent) is deployed between the application and PostgreSQL.
- Pool mode is set to
unless session-level features requiretransaction
mode.session
is calculated fromdefault_pool_size
and the number of pooler instances, not set arbitrarily.max_connections
is monitored and remains near zero during normal traffic.cl_waiting