0

Connection pool — a set of pre-opened DB connections shared across requests

Intermediate5 min read·eng-15-016
sqlperformanceinterview

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:

  1. Pre-opens a fixed number of connections at startup.
  2. When a request needs a connection, it borrows one from the pool.
  3. After the request, the connection is returned to the pool (not closed).
  4. 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
<?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');
    }
}