Connection Handling & Pooling
Connection pooling is a common topic in other languages' database documentation, but it means something different — or, in the traditional PHP model, largely doesn't apply — compared to a long-running Node.js or Java server. Understanding why comes down to how a typical PHP request is actually executed. This page explains PHP's share-nothing request lifecycle, what PDO::ATTR_PERSISTENT does and doesn't buy you, and where real connection pooling typically lives in a PHP deployment.
PHP's share-nothing request lifecycle
In the traditional PHP-FPM (or older mod_php) model, each incoming HTTP request is handled by a fresh PHP process or worker that starts with a clean slate: no memory, no open connections, and no state carried over from the previous request. The script runs from top to bottom, sends its response, and then all of its variables, including any database connection object, are destroyed. The next request — even from the same visitor a second later — starts over completely, opening its own new connection if it needs the database.
This is fundamentally different from a Node.js server, where a single long-running process handles every request and can keep a pool of database connections open in memory across all of them for the lifetime of the process. PHP's traditional model has no equivalent in-process pool, because there's no long-running process for a pool to live in — each request effectively gets its own one-shot connection unless something specifically changes that.
Persistent connections with PDO::ATTR_PERSISTENT
PDO offers one partial workaround: setting PDO::ATTR_PERSISTENT to true when connecting asks PHP to keep the underlying connection open in the PHP-FPM worker process after the script ends, so a later request handled by that same worker can reuse it instead of reconnecting from scratch.
Requesting a persistent connection
<?php
$pdo = new PDO(
'mysql:host=localhost;dbname=shop;charset=utf8mb4',
'app_user',
'secret',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => true,
]
);This can reduce the overhead of the TCP handshake and authentication that a brand-new connection requires on every single request, which matters more on databases with an expensive connect step or under very high request rates.
Why pooling is usually handled outside PHP
Because vanilla PHP-FPM has no long-running process to hold a real pool in, connection pooling for PHP applications is typically pushed down to the infrastructure layer instead of being solved in application code:
ProxySQL / PgBouncer — a lightweight proxy sits between your PHP workers and the actual database server. Each PHP worker still opens what it thinks is its own connection, but the proxy maintains a genuine pool of connections to the real database underneath and multiplexes many client connections onto far fewer real ones.
Managed database connection poolers — many hosted database platforms bundle an equivalent pooler in front of the database, so an application built on plain PDO gets pooling for free without any code change.
This division of responsibility fits PHP's execution model well: the application code stays simple (open a connection, use it, let it go), while the proxy — which does run as a long-lived process — does the actual work of maintaining and reusing a pool of real database connections.
Long-running PHP runtimes change the picture
Runtimes like Swoole and RoadRunner run PHP differently: instead of starting a fresh process per request, they keep a PHP process alive across many requests, similar to how Node.js works. In that model, an application actually can maintain its own in-process connection pool, because there's finally a long-lived process for the pool to live inside. This is a meaningful architectural shift away from traditional PHP-FPM, and it's why pooling is a "vanilla PHP-FPM rarely does this" statement rather than a "PHP can never do this" statement — it depends entirely on which runtime is executing the code.
Where connections live under each model
PHP-FPM (traditional) Swoole / RoadRunner Infra-layer pooling
------------------------ --------------------- ----------------------
Request -> new process Request -> existing Request -> PHP-FPM worker
-> new connection worker (alive) -> proxy (ProxySQL/
-> connection dies -> pooled connection PgBouncer)
at end of request reused across -> proxy holds the
many requests real pool