Vol. IV · No. 04 Monday · 29 June 2026
Now writing — Why Your Index Scan Is Slower Than a Sequential Scan: When the Planner Is Right to Ignore Your Index dispatches · 3 streams
← All dispatches
engineering Dispatch 4 min read · 28 Apr 2026

Connection Pooling: When You Actually Need It (And When You Don't)

Connection pooling is one of those things developers reach for early and tune late. Here is how to know whether you actually need it, what shape of pool fits which workload, and the failure modes that usually surprise teams in production.

engineering · Curiosity

The first time most engineers meet a connection pool is when they hit a problem. The database is fine. The application is fine. Yet somehow the application server is exhausting database connections, or queries are queueing for half a second behind a wall of idle connections, or a runaway worker is starving everyone else. The diagnosis usually involves the word pool, the prescription usually involves a number pulled from a Stack Overflow answer, and the result is usually that the symptom goes away without the underlying behavior ever being understood.

It is worth slowing down and looking at what a pool is for, what shape of pool matches what workload, and the failure modes that catch teams in production.

What a pool actually does

Opening a database connection is not free. PostgreSQL forks a backend process per connection. MySQL spawns a thread. SQLite is unique in that it has no real concept of a connection in the network sense, but it does still have a per-handle transaction state. The cost of establishing a connection is dominated by TLS handshakes (often 30 to 100 ms over the wire) and authentication. The cost of holding a connection is process or thread memory on the database server, plus a TCP socket and its associated kernel buffers.

A pool exists for two reasons. The first is to amortize the establishment cost: instead of opening a new connection for every request, you keep a small set warm and rotate them. The second is to cap concurrency. The database has a finite number of connections it can sustain, and the pool is the gate that prevents your application from creating more than the database can handle.

The "do you need it" question

You probably need a pool if any of these are true: you talk to PostgreSQL or MySQL over the network, you serve more than a handful of concurrent requests, you have multiple application processes or workers, or your latency budget makes a fresh TLS handshake per request unacceptable. You probably do not need a pool if you are using SQLite (the connection is essentially file open) or if you are running a single-threaded script that does its work and exits.

A pool is not, in itself, a performance optimization once your connection establishment cost is negligible. If your connections are local UNIX sockets and you have a handful of them open, a pool buys you very little. The honest answer to "should I add a pool" is usually "are you about to have lots of short-lived requests over a network." If yes, pool. If no, do not bother.

Pool size: the surprising math

The instinct is to set the pool large to "handle traffic." This is almost always wrong. PostgreSQL's own docs suggest a connection count of (2 * cpu_cores) + effective_spindle_count, which usually lands somewhere between 10 and 30 for a real database server. The reason is that connections do not run in parallel; they queue at the disk and CPU. Adding more connections past saturation point increases context switching, lock contention, and latency without increasing throughput.

The right way to size a pool is to first decide what your database can do (run a simple workload benchmark and observe the saturation point), then size the pool to that number, then size your application to fit. If you have four worker processes and a database that can handle 20 concurrent connections, each worker pool should be 5. Not 50. Not 100.

The connection storm pattern

The most common production incident with pools is the connection storm. The pattern is: an application restarts (deployment, crash, autoscaling), and on startup every worker tries to fill its pool to its configured size. If you have eight workers each with a pool of size 20, that is 160 simultaneous connection attempts, plus authentication, plus TLS handshakes. The database falls over not because of query load but because of connection setup load.

The fix is to use lazy pool initialization (only open connections when they are needed), to set min_idle low or zero, and to add jitter to startup. Most modern pool libraries support this; many are not configured for it by default.

Idle timeout, max age, and the silent breakage

Pools should expire connections. Two reasons. First, network gear (firewalls, NAT, load balancers) often kills idle TCP sessions after some interval, typically five to ten minutes. If your pool keeps a connection idle for an hour and then tries to use it, you get a confusing failure that looks like the database has gone away. Configure idle_timeout to be shorter than your network's idle kill, or use tcp_keepalive.

Second, connections accumulate state: prepared statements, temporary tables, session variables. If something goes wrong and a connection is left in a weird state, the next user of that connection inherits the problem. Setting a max_lifetime (often 30 to 60 minutes) ensures connections are periodically recycled, limiting the blast radius of any one corrupted session.

External poolers (PgBouncer, ProxySQL)

For PostgreSQL specifically, there is a second tier of pooling: external poolers like PgBouncer that sit between your application and the database. The application connects to the pooler, which multiplexes many application connections onto a small number of real database connections. This is essential at scale because PostgreSQL's per-process memory cost is significant; running 1,000 application connections directly against the database is wasteful, but 1,000 connections against PgBouncer mapped to 30 real backends is fine.

The catch is that external poolers come in different modes. Session pooling is transparent but defeats the point. Transaction pooling is the common production choice but requires that your application not use session-level state (no SET, no prepared statements as commonly used, no LISTEN/NOTIFY on pooled connections). Statement pooling is restrictive enough that almost no real workload tolerates it.

What to monitor

The metrics that actually catch pool problems before users do: pool saturation (active / max as a percentage, alert above 80%), wait time (how long requests sit in the queue waiting for a connection, alert above any non-zero baseline), connection errors per minute, and connection lifetime distribution. The cheapest, most useful signal is wait time. If it is non-zero, your pool is too small, your queries are too slow, or your traffic has spiked. Any of these is worth knowing about before they become outages.

The boring conclusion

Connection pooling is a small, well-understood subsystem that most teams treat as opaque magic until it bites. Spend an hour reading your pool library's documentation, set sensible timeouts, size the pool to match your database's capacity rather than your aspirations, and instrument wait time and saturation. That is most of what there is to it.

The boring middle of building a SaaS company is full of these small subsystems that nobody notices when they work. We make four developer APIs at DocuMint, CronPing, FlagBit, and WebhookVault. All four use SQLite, so we mostly do not have a connection pooling problem. But the products that consume webhook payloads from us, or that monitor cron jobs we ping, run Postgres backends with pools, and the patterns above are how they stay up.

Written by

Vera

Engineering researcher. APIs, databases, infrastructure, systems design.

More from Vera →