Reducing PostgreSQL Connection Time from 329 ms to 2 ms
A practical guide to PgBouncer connection pooling
The Problem
Our baseline benchmark recorded an average connection time of 329ms. Establishing database connections was a significant source of overhead in the platform.
A new PostgreSQL connection involves network setup and authentication. I investigated connection pooling to reduce how often the application needed to pay that cost.
Why Connection Pooling?
Connection pooling maintains a set of pre-established database connections that can be reused across requests. Instead of paying the connection overhead every time, you grab an existing connection from the pool, use it, and return it.
PgBouncer is a lightweight connection pooler for PostgreSQL. It sits between your application and database, managing connection lifecycle transparently.
The Implementation
Step 1: Baseline Measurement
Before optimizing anything, I needed numbers. My benchmarking work covered:
- Connection time: How long to establish a new connection
- Query execution: Time for actual database operations
- Concurrent load: Performance under realistic multi-user scenarios
# Minimal connectivity check, not the full benchmark harness
for i in {1..100}; do
time psql -h localhost -c "SELECT 1"
done
This small example times the complete client invocation and query; it does not isolate connection establishment. The recorded benchmark results below describe connection time and load testing separately.
Baseline results:
- Average connection time: 329ms
- p99 connection time: 412ms
- Max concurrent connections before degradation: ~50
Step 2: PgBouncer Configuration
The key configuration decisions:
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
# Authentication
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
Why transaction mode? In transaction pooling, connections are assigned per-transaction rather than per-session. This provides the best connection reuse for typical web applications where requests are short-lived.
Step 3: Application Changes
The application needed minimal changes—just point to PgBouncer instead of PostgreSQL directly:
// Before
const pool = new Pool({
host: 'postgres.internal',
port: 5432
})
// After
const pool = new Pool({
host: 'pgbouncer.internal',
port: 6432
})
The Results
After deployment:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Connection time | 329ms | 2ms | 99.4% |
| Concurrent capacity | 50 | 280+ | 458% |
| Throughput (qps) | baseline | +6.3% | - |
| Complex queries | baseline | 13% faster | - |
The 2ms connection time is essentially just the network round-trip to PgBouncer. The actual PostgreSQL connection is already established and waiting.
Lessons Learned
1. Measure Before You Optimize
Without baseline numbers, I couldn't have quantified the improvement or justified the infrastructure change. The benchmarking script became a permanent part of our toolkit.
2. Transaction Mode Has Tradeoffs
Transaction pooling needs care around features that depend on a particular server session:
- Protocol-level named prepared statements require a supported PgBouncer version and a nonzero
max_prepared_statementssetting. SQL-levelPREPAREandEXECUTEhave different limitations. - Session-level settings (
SETcommands) - Session-level advisory locks
See the PgBouncer configuration reference for the version-specific behavior. This is distinct from transaction-scoped advisory locks.
We had to audit our codebase for these patterns before deploying.
3. Documentation is Part of the Deliverable
I created runbooks for:
- How to monitor PgBouncer (
SHOW POOLS,SHOW STATS) - How to run the benchmark suite
- Troubleshooting common issues
This ensures the next engineer can maintain and improve the system.
Conclusion
Connection pooling is one of those optimizations with an outsized impact-to-effort ratio. A few hours of configuration yielded a 99.4% latency reduction that improves every database operation in the system.
The key is approaching it systematically: measure first, understand the tradeoffs, and document everything for the engineers who come after you.