Connection pool — a set of pre-opened DB connections shared across requests
Concept
Connection pool — a set of pre-opened, reusable database connections maintained and shared across multiple requests or threads, avoiding the overhead of opening a new connection for every request.
Why connections are expensive: Opening a database connection involves TCP handshake, authentication, session setup — typically 20-100ms. For a PHP-FPM app serving 500 requests/second, opening a new connection per request would be 500 connections per second, overwhelming the DB server.
What a connection pool does:
- Pre-opens a fixed number of connections at startup.
- When a request needs a connection, it borrows one from the pool.
- After the request, the connection is returned to the pool (not closed).
- Other requests can reuse it immediately.
PHP-FPM and connection pooling: PHP-FPM workers are persistent processes. Each FPM worker holds its own persistent connection (via pconnect in PDO or MYSQL_ATTR_INIT_COMMAND). This is a simplified per-worker pool. True connection pooling (shared across workers) requires PgBouncer (PostgreSQL) or ProxySQL (MySQL).
Connection pool settings in Laravel (config/database.php):
pool.min: Minimum connections to keep open.pool.max: Maximum connections.wait_timeout: How long to wait if all connections are in use.idle_timeout: Close idle connections after this many seconds.
ProxySQL / PgBouncer: External proxy servers that pool connections between the application and the DB. The app connects to the proxy; the proxy maintains a smaller pool of actual DB connections. Scales to thousands of app connections with only hundreds of actual DB connections.
Code Example
<?php
// Laravel database config — connection pool settings
// config/database.php
return [
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'options' => [
// PDO persistent connection (per FPM worker "pool")
\PDO::ATTR_PERSISTENT => true,
],
],
],
];
// With Swoole/Octane — true connection pool (shared across coroutines)
// config/octane.php
return [
'swoole' => [
'options' => [
'worker_num' => 4,
],
],
];
// Laravel Octane manages a connection pool per worker
// In Octane context, DB connections are pooled across requests in the same worker
// CHECKING connection pool exhaustion
try {
$result = \DB::select('SELECT 1');
} catch (\Illuminate\Database\QueryException $e) {
if (str_contains($e->getMessage(), 'Too many connections')) {
// Pool exhausted — check:
// 1. max_connections in MySQL (SHOW VARIABLES LIKE 'max_connections')
// 2. Number of FPM workers × connections per worker
// 3. Use PgBouncer/ProxySQL to pool externally
}
}
// Checking current connections in MySQL
// SHOW STATUS LIKE 'Threads_connected'; — current active connections
// SHOW PROCESSLIST; — see what each connection is doing
// ProxySQL setup (external, not PHP code):
// App → ProxySQL (port 6033) → MySQL (port 3306)
// ProxySQL pools 10 actual connections, serves 1000 app connections
// .env: DB_HOST=proxysql-host, DB_PORT=6033
// Long-running processes (queue workers, scripts) — reconnect on connection loss
class WorkerWithReconnect
{
public function run(): void
{
while (true) {
try {
$job = \DB::table('jobs')->first();
if (!$job) { sleep(1); continue; }
$this->process($job);
} catch (\Illuminate\Database\QueryException $e) {
if ($this->isConnectionLost($e)) {
\DB::reconnect(); // re-establish the connection
continue;
}
throw $e;
}
}
}
private function isConnectionLost(\Throwable $e): bool
{
return str_contains($e->getMessage(), 'server has gone away')
|| str_contains($e->getMessage(), 'Lost connection');
}
}