Connection Pooling Benchmarks
The cost of arriving without a reservation. What happens when 200 clients show up at a database that seats 100, and how pooling changes the arithmetic.
Methodology
These benchmarks isolate connection overhead from query execution time. The test query is SELECT 1 — the simplest possible query, ensuring that all measured latency is connection and protocol overhead, not database work.
The "direct" mode opens a new PostgreSQL connection for every query (TCP + TLS + authentication), executes the query, and closes the connection. The "pooled" mode routes through Gold Lapel's built-in connection pool, which maintains persistent backend connections to PostgreSQL.
| Parameter | Value |
|---|---|
| PostgreSQL | 16.2 (max_connections = 100) |
| Gold Lapel | 0.1.0 (built-in connection pooling) |
| CPU | 4-core AMD EPYC (c5.xlarge) |
| RAM | 8 GB |
| Storage | gp3 SSD, 3000 IOPS |
| Network | Same VPC, <1ms RTT |
| Test query | SELECT 1 (measures connection overhead only) |
| Duration | 60 seconds per test |
Where the time goes
A direct PostgreSQL connection involves a TCP handshake, TLS negotiation, and the PostgreSQL authentication protocol — before the first byte of SQL is parsed. Gold Lapel's pool eliminates all three for every query after the first.
# Direct connection: each request opens a new TCP + SSL + PG handshake
# Time breakdown per connection:
# TCP handshake: 0.3ms
# TLS negotiation: 1.2ms
# PG authentication: 0.8ms
# Query execution: 0.1ms
# Connection close: 0.1ms
# Total: 2.5ms (connection overhead dominates)
# Pooled connection: reuses existing authenticated connection
# Time breakdown per query:
# Query dispatch: 0.1ms (connection already open)
# Query execution: 0.1ms
# Result return: 0.1ms
# Total: 0.3ms Results
| Concurrent clients | Direct TPS | Pooled TPS | Direct latency (P50) | Pooled latency (P50) | Throughput gain |
|---|---|---|---|---|---|
| 10 | 2,840 | 9,420 | 3.5ms | 1.1ms | 3.3x |
| 50 | 4,120 | 34,800 | 12.1ms | 1.4ms | 8.5x |
| 100 | 3,890 | 38,200 | 25.7ms | 2.6ms | 9.8x |
| 200 | Connection refused | 36,400 | N/A | 5.5ms | - |
| 500 | Connection refused | 33,100 | N/A | 15.1ms | - |
At 200+ concurrent clients, direct connections are refused because PostgreSQL's max_connections is set to 100. The pool multiplexes 500 application clients onto 20 backend connections.
Analysis
The throughput improvement scales with concurrency because the bottleneck shifts from connection creation to connection contention.
- At 10 clients: Direct connections are viable. Each client gets a dedicated backend connection, and the overhead per connection (2.5ms) is tolerable. Pooling still provides a 3.3x improvement by eliminating per-query connection setup.
- At 50-100 clients: The improvement curve steepens (8.5x–9.8x). Direct connections now compete for PostgreSQL's connection slots and memory. Each backend connection consumes approximately 10 MB of RAM, so 100 connections consume 1 GB — a significant fraction of the 8 GB available.
- At 200+ clients: Direct connections fail entirely. The pool continues serving requests by queuing application clients and dispatching them to available backend connections. Latency increases (5.5ms at 200 clients, 15.1ms at 500) but remains far below direct connection overhead, and no requests are refused.
Pool sizing
Gold Lapel's default pool size is min(4 * CPU_cores, max_connections / 2). For this benchmark (4 cores, max_connections=100), the pool maintains 16 backend connections. This is sufficient for 500 concurrent clients because the test query executes in under 1ms — each backend connection can serve hundreds of queries per second.
For real workloads with longer query execution times, the pool size should be tuned based on the expected concurrent query volume and average query duration. The pool automatically adjusts between a minimum of 4 and the configured maximum.
How to reproduce
# Clone the benchmark suite
git clone https://github.com/goldlapel/benchmarks
cd benchmarks/connection-overhead
# Start test environment (PG with max_connections=100)
docker compose up -d
# Run direct connection benchmark
./run.sh --mode direct --clients 10,50,100,200,500 --duration 60
# Run pooled connection benchmark (through Gold Lapel)
./run.sh --mode pooled --clients 10,50,100,200,500 --duration 60
# Compare results
./compare.sh results/ The benchmark uses pgbench in direct mode and a custom client for pooled mode. The comparison script generates a CSV report with latency percentiles.
Terms referenced in this article
The pool sizing question these numbers raise has a practical treatment for every major framework. If you are running Spring Boot, the open-in-view pool exhaustion guide shows how the default configuration defeats even a well-sized pool. If Rails, the connection pool sizing guide reveals the arithmetic behind Puma and Solid Queue.