skip to content

A PHP CLI script opens a PDO connection and then calls pcntl_fork() to start four workers that each run queries; what breaks, and how do you fix it?

level: seniorimportance: nice to knowfreq 22%

answer

  1. copied memory, shared descriptors
  2. one socket, five speakers
  3. child exit() runs destructors
  4. connect after the fork, per process
  5. output buffers and PRNG state copied

basics

~20 s

Every child inherits the same database socket, so their queries interleave on one connection and corrupt it, and a child's exit can close the session the parent still uses. Open connections after pcntl_fork(), separately in each process.

solid answer

~40 s

`pcntl_fork()` copies the PHP heap but shares open file descriptors, so the `PDO` object in each child is a copy that points at the **same** socket. Five processes then write queries and read results on one connection: replies go to whichever process reads first, results get mixed up, and the driver reports protocol errors. When a child calls `exit()`, its destructors run, and the driver's disconnect can end the server-side session the parent still relies on. The fix is to fork first and connect afterwards: each child opens its own connection, and the parent either reconnects or never opened one before forking. The same rule applies to any socket, open file or queue client created before the fork.

code

php · 25 lines
php
<?php
declare(strict_types=1);

$dsn = 'mysql:host=db;dbname=app';
$children = [];

for ($i = 0; $i < 4; $i++) {
    $pid = pcntl_fork();
    if ($pid === -1) {
        exit(1);
    }
    if ($pid === 0) {
        // child: its own connection, opened after the fork
        $pdo = new PDO($dsn, 'worker', getenv('DB_PASSWORD') ?: '');
        runWorker($pdo);
        exit(0);
    }
    $children[] = $pid;
}

// parent opens its own connection only now, if it needs one
$pdo = new PDO($dsn, 'supervisor', getenv('DB_PASSWORD') ?: '');
foreach ($children as $pid) {
    pcntl_waitpid($pid, $status);
}

go deeper

for a junior

Recall that a fork copies PHP variables but not the network connections behind them: each process must open its own database connection after pcntl_fork().

for a middle

Explain the difference between copied memory and shared file descriptors, and list the symptoms of two processes using one database socket.

for a senior

Diagnose intermittent protocol errors and 'connection closed' failures in forked workers, trace them to eagerly opened connections and destructors on exit, and restructure boot so connections open after the fork.

for a principal

Weigh forking inside one PHP process against running independent worker processes under a manager, where no connection can leak across a fork in the first place.

## What a fork copies and what it shares `pcntl_fork()` creates a child that is a copy of the parent at the moment of the call. That copy has two very different parts: - **Memory is copied.** Every PHP variable, array and object exists separately in each process. A change in one is invisible in the other. - **File descriptors are shared.** An open socket, file or pipe is represented in each process by a descriptor that points to the **same** underlying kernel object. Reading from it in one process consumes data the other process will never see. A `PDO` object holds a database connection, which is a network socket. After the fork, the parent and every child each have their own `PDO` object — but all of them talk through one socket to one server-side session. ## What goes wrong with the shared connection 1. **Interleaved traffic.** Two children send queries at nearly the same moment. The server answers them in order on the one connection, but either child may read either answer. One child gets the other's result set; another sees a half-read packet and fails with a protocol error. 2. **Shared session state.** Transactions, session variables and prepared statements belong to the server-side session. A `beginTransaction()` in one child can end up wrapping queries that other processes send. 3. **Destructors on exit.** When a child ends with `exit()`, PHP runs shutdown functions and object destructors as usual. The child's `PDO` destructor disconnects, and the driver's goodbye on the shared socket can end the session for everyone. The parent's next query then fails because the server has closed its connection. These failures are intermittent and load-dependent, which makes them hard to reproduce. ## The fix: connect after forking - **Fork first, connect second.** The parent forks the workers before opening any connection; each child creates its own `PDO` inside the child branch. - **If the parent already has a connection,** drop it before forking (set the variable to `null` so its destructor runs in the parent alone) and reconnect afterwards; each child then connects on its own. - **Keep persistent connections out of forked code.** A persistent PDO connection is reused from a process-level pool, so a child can pick up the very handle the parent was using. ## Other state that fork duplicates | Inherited thing | Symptom | Remedy | |---|---|---| | queue, cache or HTTP client sockets | mixed replies, broken protocol | create clients after the fork | | open file handles with a shared offset | interleaved or overwritten log lines | open files per process, or append with a lock | | output buffered with `ob_start()` | the same text printed by parent and child | flush buffers before forking | | shutdown functions and destructors | cleanup (deleting temp files, sending a "done" message) runs in every child | guard with a check of `posix_getpid()` against the PID that registered it | | an already-used `mt_rand()` state | children draw identical "random" sequences | reseed per child, or use `random_int()`, which reads the operating system's generator | | `pcntl_signal()` handlers | children react to signals like the parent | re-register the child's own handlers after the fork | ## Why this is a PHP-specific trap In a normal PHP-FPM request, every connection is opened and closed within one request in one process, so a developer rarely thinks about descriptor ownership. Frameworks and service containers often open a database connection eagerly while booting — before the code that forks even runs. A CLI command that boots the application and then forks workers inherits that connection without any line in the command mentioning it. Checking what the bootstrap opens is part of writing forking code. ## A safe structure - Boot only configuration in the parent; defer creating connections. - Fork the workers. - In each child: create connections, install the child's signal handlers, run the loop, then `exit()`. - In the parent: install the supervisor's handlers, reap children with `pcntl_waitpid()`, and forward `SIGTERM` with `posix_kill()` on shutdown.

  • Why can a child's exit() break the parent's connection even if the child never ran a query?
    `exit()` runs destructors in the child, including the copied `PDO` object's. Its disconnect goes out over the socket the child shares with the parent, so the server can end that session. The parent keeps a descriptor to a connection the server has closed and fails on its next query.
  • A framework command boots the app and then forks; which inherited objects do you check first?
    Anything that opened a descriptor during boot: database and cache connections, queue and HTTP clients, open log files, and output buffers. Also any registered shutdown functions, since they run again in each child. Defer these until after the fork or recreate them inside each child branch.

saying these in an interview costs you the question

  • Each child gets its own fresh database connection automatically after pcntl_fork()
  • Children share the parent's PHP variables, so one PDO object serves all of them
  • Persistent PDO connections make sharing across forked children safe
  • exit() in a child skips destructors and shutdown functions
  • Closing the connection in one child has no effect on the others