Connection Pooling
debt(d5/e5/b5/t5)
Closest to 'specialist tool catches' (d5). The term's detection_hints.tools lists mysql-processlist, pgbouncer, proxysql, and datadog — all specialist monitoring/infrastructure tools. Connection pooling issues manifest as 'Too many connections' errors or slow time-to-first-byte, requiring database monitoring or APM tools to diagnose, not standard linters or compilers.
Closest to 'touches multiple files / significant refactor' (e5). The quick_fix mentions configuring PHP-FPM pm.max_children and introducing PgBouncer or ProxySQL. This isn't a one-line code change — it requires infrastructure configuration, potentially new services (connection pooler), and adjusting application connection strings across the codebase. A meaningful fix spans ops config and application code.
Closest to 'persistent productivity tax' (b5). Connection pooling strategy applies to web and queue-worker contexts per applies_to, affecting all database-touching code paths. The choice of pooling approach (or lack thereof) creates ongoing operational overhead — pool sizing, timeout tuning, stale connection handling. It's not architectural-level burden (b7+), but it's more than localized since every request's DB behavior depends on this configuration.
Closest to 'notable trap / documented gotcha' (t5). The misconception explicitly states that developers wrongly believe PHP persistent connections (pconnect) are equivalent to proper connection pooling. This is a documented gotcha — pconnect is per-FPM-worker, not a shared pool. Developers familiar with connection pooling from other ecosystems expect shared pools and are surprised by PHP's per-process model.
Also Known As
TL;DR
Explanation
Opening a database connection is expensive — TCP handshake, authentication, and protocol negotiation can add 10–100ms per request. Connection pooling maintains a pool of open connections that are borrowed and returned rather than created and destroyed. In traditional PHP-FPM setups, each PHP process maintains its own persistent connection (PDO with persistent=true or using PgBouncer/ProxySQL as a pool proxy). This is critical at scale — without pooling, databases hit connection limits under moderate load.
Diagram
flowchart LR
subgraph Without Pool
REQ1[Request] -->|new connection| DB1[(DB)]
REQ2[Request] -->|new connection| DB1
REQ3[Request] -->|new connection| DB1
INFO1[Each: TCP handshake<br/>auth overhead ~50ms]
end
subgraph With Pool
POOL[(Connection Pool<br/>10 persistent connections)]
R1[Request] -->|borrow| POOL
R2[Request] -->|borrow| POOL
POOL <-->|persistent| DB2[(DB)]
INFO2[Borrow/return O of 1<br/>no handshake overhead]
end
style INFO1 fill:#f85149,color:#fff
style INFO2 fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- PHP-FPM persistent connections are per-worker, not shared — each worker holds its own connection.
- Not using a dedicated pooler like PgBouncer for PostgreSQL — PHP's built-in pooling is limited.
- Pool connections that are never validated — stale connections that have been closed by the database cause errors.
- Setting connection timeout too high — idle connections held open indefinitely consume database server resources.
Code Examples
// New connection on every request — no pooling:
function getDb(): PDO {
return new PDO('mysql:host=db;dbname=app', 'user', 'pass');
// TCP connect + auth on every request — 5-20ms overhead per request
}
// Fix: use persistent connections or a connection pool
// PHP-FPM does NOT persist connections between requests by default
// Use a pooler (pgBouncer/ProxySQL) at the infrastructure level
// Point your DSN at the pooler, not the DB directly:
$dsn = 'pgsql:host=pgbouncer;port=6432;dbname=myapp';
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_PERSISTENT => false, // let pgBouncer manage pooling
]);
// pgBouncer config (pgbouncer.ini)
[databases]
myapp = host=postgres port=5432 dbname=myapp
[pgbouncer]
pool_mode = transaction ; most efficient
max_client_conn = 1000 ; FPM workers
default_pool_size = 20 ; actual DB connections