PHP7
Asynchronous Programming
Multithreading
FastCGI Process Manager
Web Development

Async/Thread on PHP7 with FPM

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

PHP-FPM (FastCGI Process Manager) uses a process-based model where each request runs in a separate process with no shared memory. PHP does not support threads in FPM mode — the pthreads extension only works with the CLI SAPI, not with web server SAPIs. For async behavior in PHP-FPM, use non-blocking I/O libraries like ReactPHP or Amp, offload work to message queues (Redis, RabbitMQ), use exec() to spawn background processes, or leverage PHP 8.1+ Fibers for cooperative multitasking.

Why Threads Don't Work in FPM

php
1// This does NOT work under PHP-FPM
2// pthreads extension is CLI-only
3
4class MyThread extends Thread {
5    public function run() {
6        echo "Thread running";
7    }
8}
9
10$thread = new MyThread();
11$thread->start();  // Fatal error in FPM: Class 'Thread' not found

PHP-FPM spawns worker processes, not threads. Each request gets its own process. The pthreads extension is explicitly disabled for web SAPIs because PHP's core data structures are not thread-safe.

Solution 1: ReactPHP (Event-Driven I/O)

ReactPHP provides an event loop for non-blocking I/O:

bash
composer require react/http react/promise
php
1use React\EventLoop\Factory;
2use React\Http\Browser;
3
4$loop = Factory::create();
5$client = new Browser($loop);
6
7// Fire multiple HTTP requests concurrently
8$promises = [
9    $client->get('https://api.example.com/users'),
10    $client->get('https://api.example.com/orders'),
11    $client->get('https://api.example.com/products'),
12];
13
14\React\Promise\all($promises)->then(function (array $responses) {
15    foreach ($responses as $response) {
16        echo $response->getBody() . "\n";
17    }
18});
19
20$loop->run();

All three requests run concurrently on a single process using non-blocking sockets. The event loop handles I/O multiplexing.

Solution 2: Amp (Coroutines)

Amp provides async/await style programming for PHP:

bash
composer require amphp/http-client amphp/amp
php
1use Amp\Http\Client\HttpClientBuilder;
2use Amp\Http\Client\Request;
3use function Amp\async;
4use function Amp\Future\await;
5
6$client = HttpClientBuilder::buildDefault();
7
8// Run requests concurrently using async/await
9$futures = [
10    async(fn() => $client->request(new Request('https://api.example.com/users'))),
11    async(fn() => $client->request(new Request('https://api.example.com/orders'))),
12];
13
14$responses = await($futures);
15
16foreach ($responses as $response) {
17    echo $response->getBody()->buffer() . "\n";
18}

Solution 3: Background Process with exec()

Fire-and-forget a background job from an FPM request:

php
1// Spawn a background PHP process
2exec('php /path/to/worker.php --job=send-email [email protected] > /dev/null 2>&1 &');
3
4// Return immediately to the client
5echo json_encode(['status' => 'queued']);
php
1// worker.php — runs independently of the web request
2$options = getopt('', ['job:', 'to:']);
3
4switch ($options['job']) {
5    case 'send-email':
6        sendEmail($options['to']);
7        break;
8}

The & at the end makes the process run in the background. The web request returns immediately while the worker runs independently.

Use a message broker to decouple web requests from background processing:

php
1// Web request: push job to Redis queue
2$redis = new Redis();
3$redis->connect('127.0.0.1');
4$redis->lPush('email_queue', json_encode([
5    'to' => '[email protected]',
6    'subject' => 'Welcome',
7    'body' => 'Hello!'
8]));
9
10echo json_encode(['status' => 'queued']);
php
1// Worker (runs as a separate long-running CLI process)
2$redis = new Redis();
3$redis->connect('127.0.0.1');
4
5while (true) {
6    $job = $redis->brPop('email_queue', 30);  // Block for 30s
7    if ($job) {
8        $data = json_decode($job[1], true);
9        sendEmail($data['to'], $data['subject'], $data['body']);
10    }
11}

Run the worker with supervisord or systemd to keep it alive:

ini
1# /etc/supervisor/conf.d/email-worker.conf
2[program:email-worker]
3command=php /path/to/worker.php
4numprocs=4
5autostart=true
6autorestart=true

Solution 5: PHP 8.1 Fibers

Fibers provide cooperative multitasking within a single process:

php
1// PHP 8.1+
2$fiber1 = new Fiber(function () {
3    echo "Fiber 1: start\n";
4    Fiber::suspend('fiber1-data');
5    echo "Fiber 1: resumed\n";
6});
7
8$fiber2 = new Fiber(function () {
9    echo "Fiber 2: start\n";
10    Fiber::suspend('fiber2-data');
11    echo "Fiber 2: resumed\n";
12});
13
14// Interleave execution
15$result1 = $fiber1->start();   // "Fiber 1: start"
16$result2 = $fiber2->start();   // "Fiber 2: start"
17$fiber1->resume();              // "Fiber 1: resumed"
18$fiber2->resume();              // "Fiber 2: resumed"

Fibers are the foundation for async libraries like Amp v3 and RevoltPHP. They allow cooperative scheduling but are not true threads — they run on a single OS thread.

Solution 6: curl_multi for Parallel HTTP

php
1$urls = [
2    'https://api.example.com/users',
3    'https://api.example.com/orders',
4    'https://api.example.com/products',
5];
6
7$multi = curl_multi_init();
8$handles = [];
9
10foreach ($urls as $url) {
11    $ch = curl_init($url);
12    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
13    curl_multi_add_handle($multi, $ch);
14    $handles[] = $ch;
15}
16
17// Execute all requests concurrently
18do {
19    curl_multi_exec($multi, $running);
20    curl_multi_select($multi);
21} while ($running > 0);
22
23// Collect results
24foreach ($handles as $ch) {
25    echo curl_multi_getcontent($ch) . "\n";
26    curl_multi_remove_handle($multi, $ch);
27    curl_close($ch);
28}
29
30curl_multi_close($multi);

curl_multi runs HTTP requests in parallel using non-blocking I/O, without requiring any external libraries.

Common Pitfalls

  • Trying to use pthreads in FPM: The pthreads extension only works with the CLI SAPI. It is explicitly incompatible with FPM, Apache mod_php, and other web server SAPIs. There is no workaround — PHP's core is not thread-safe under these SAPIs.
  • Using exec() without output redirection: exec('php worker.php') without > /dev/null 2>&1 & blocks until the worker finishes, making the web request wait. Always redirect output and background the process.
  • Not handling queue worker failures: If a queue worker crashes, messages are lost unless the queue supports acknowledgments. Use RabbitMQ with manual acks or Redis with BRPOPLPUSH for reliable processing.
  • Running event loop libraries inside FPM requests: ReactPHP and Amp event loops are designed for long-running CLI processes. Running them inside a short-lived FPM request adds overhead and complexity. Use them for CLI workers, not web requests.
  • Confusing Fibers with threads: Fibers are cooperative (they explicitly yield), run on a single OS thread, and do not provide parallelism. They enable async-style code but cannot use multiple CPU cores. For CPU-bound parallelism, use multiple worker processes.

Summary

  • PHP-FPM does not support threads — pthreads is CLI-only
  • Use curl_multi for parallel HTTP requests within a single FPM request
  • Use ReactPHP or Amp for event-driven async I/O in CLI workers
  • Offload background work to message queues (Redis, RabbitMQ) with separate worker processes
  • PHP 8.1 Fibers enable cooperative multitasking but are not true threads
  • For production async workloads, use message queues with supervisor-managed workers

Course illustration
Course illustration

All Rights Reserved.