Asynchronous PHP: Swoole and ReactPHP Compared
AI generated
<?php
8.4
PHP · Async · Swoole · ReactPHP
Asynchronous PHP: Swoole and ReactPHP as an Alternative
to the classic request-response model

PHP-FPM processes requests synchronously and blocking, a model that is well proven for classic web applications but reaches its limits with WebSockets, queue workers and heavy I/O load. Asynchronous PHP with Swoole or ReactPHP uses event loops and coroutines to manage thousands of concurrent connections in a single process, without ever leaving the PHP language.

16 min read Swoole · ReactPHP · Event Loop · Coroutines PHP 8.4

1. Why classic PHP works synchronously

The classic execution model of PHP is tied to the HTTP request-response cycle. A web server such as nginx accepts a request and passes it on via FastCGI to a PHP-FPM worker process. That worker executes the script completely, returns a response, and is then available again for the next request. Every blocking operation, a database query, an HTTP call to an external API, a filesystem operation, halts the entire worker process. While the worker is waiting for the answer, it cannot handle any other request.

This model is deliberately built this way because it isolates state per request and is therefore exceptionally robust against errors in individual requests. If a request crashes, only a single worker is terminated and restarted by the process manager, while the rest of the application keeps running unaffected. Scaling happens horizontally through the number of worker processes: more concurrent requests mean more workers, more memory usage and more CPU context switches. For classic, short-lived web requests with manageable I/O wait time, this works excellently and is the reason why PHP-FPM has been the backbone of production PHP applications for decades.

This model only becomes problematic when many connections stay open for a long time or a lot of time is spent waiting rather than computing. A WebSocket server with ten thousand concurrent connections would, in a pure PHP-FPM model, require ten thousand permanently occupied worker processes, each with its own memory overhead. This is exactly where asynchronous PHP comes in: instead of blocking one process per connection, a single process manages thousands of connections at once through an event loop, by never waiting on a single I/O operation but switching between ready tasks instead.

2. Event loop basics: how asynchronous PHP technically works

The central building block of asynchronous PHP is the event loop. Instead of executing an I/O operation synchronously and waiting for its result, the code registers a callback function for the moment the operation completes, and immediately returns control to the event loop. The event loop itself is at its core an endless loop that continuously checks which registered I/O resources, file descriptors, sockets or timers, are currently ready, and runs the associated callbacks. This principle is called non-blocking I/O: the process never blocks on a single operation, but actively polls what needs attention right now.

Technically this relies on operating system mechanisms such as select(), poll() or the more efficient epoll() on Linux. These system calls allow monitoring multiple file descriptors at once and only block until at least one of them is readable or writable, instead of polling each one individually in sequence. PHP itself already provides the basic building blocks for non-blocking I/O through its stream API and functions like stream_select(), even without an additional extension. ReactPHP builds directly on top of this, while Swoole brings its own event loop written in C with coroutine support, which is considerably more performant than a pure PHP implementation.

It is important to understand that an event loop fundamentally runs single-threaded. At any given moment only one piece of PHP code is running, so there are no classic race conditions caused by concurrent memory access, as known from multithreading. Concurrency arises solely from switching between waiting operations, while actual computation remains strictly sequential. This makes asynchronous PHP extremely efficient for I/O-heavy workloads, but unsuitable for distributing pure CPU load across multiple cores, which still requires separate processes or threads.


<?php
declare(strict_types=1);

// Minimal illustrative event loop concept using stream_select().
// This is not production code, it demonstrates the core mechanism
// behind asynchronous PHP: never block on a single stream.

$sockets = [/* array of non-blocking stream resources */];
$callbacks = [];

function registerCallback(array &$callbacks, $socket, callable $onReadable): void
{
    $callbacks[(int) $socket] = $onReadable;
}

// The event loop: runs until there is nothing left to watch
while (!empty($sockets)) {
    $read = $sockets;
    $write = null;
    $except = null;

    // Blocks only until at least one socket is ready, not per-socket
    $ready = stream_select($read, $write, $except, 5);

    if ($ready === false) {
        break; // interrupted by a signal, handle and continue in real code
    }

    foreach ($read as $socket) {
        $callback = $callbacks[(int) $socket] ?? null;
        if ($callback !== null) {
            $callback($socket); // run only the code for the ready socket
        }
    }
}

3. ReactPHP in detail: event loop, promises and streams

ReactPHP is a pure PHP library, not a server extension, and can therefore be installed in any PHP project via Composer without changing the PHP installation itself. The core package react/event-loop provides the central event loop, to which timers, stream watchers and signal handlers can register. All other ReactPHP components, HTTP server, socket client, DNS resolver, child process management, are built on top of it and share the same loop instance. The result is a modular ecosystem where you only include the packages you actually need.

Asynchronous operations in ReactPHP are modeled through promises, implemented in the react/promise package. A promise represents the future result of an operation that has not yet completed, and offers methods like then() to register callbacks for success and failure. Unlike synchronous code, where a return value is immediately available, control flow in asynchronous PHP with ReactPHP is expressed through chained callbacks. Since PHP 8.1 this can be combined with fibers, so promise chains can also be written in a synchronous-looking style with await()-like helper functions, which counteracts the classic problem of deeply nested callbacks.

Streams are the third central concept: react/stream abstracts readable and writable data streams, for example for HTTP bodies, files or TCP connections, and emits events such as data, end or error as soon as new data is available. This design allows processing large amounts of data without holding it entirely in memory, an advantage over many synchronous PHP APIs that rely on fully buffered strings. For developers coming from the classic PHP-FPM world, ReactPHP is often the gentler entry point into asynchronous PHP, because no server extension needs to be installed and the code stays in familiar PHP syntax.


<?php
declare(strict_types=1);

require 'vendor/autoload.php';

use React\EventLoop\Loop;
use React\Http\Browser;

// A simple timer registered on the global event loop
Loop::addPeriodicTimer(10.0, function (): void {
    echo "Heartbeat: still running\n";
});

// Asynchronous HTTP request, returns a Promise instead of blocking
$browser = new Browser();

$browser->get('https://api.example.com/status')
    ->then(function (Psr\Http\Message\ResponseInterface $response): void {
        // Runs once the response arrives, without blocking the loop
        echo "Status: " . $response->getStatusCode() . "\n";
    })
    ->catch(function (Throwable $error): void {
        // Runs on failure, network error or non-2xx handling upstream
        echo "Request failed: " . $error->getMessage() . "\n";
    });

// Nothing above blocks execution, the loop keeps running until
// there are no more pending timers or I/O watchers left
Loop::run();

4. Swoole in detail: coroutines and the server model

Swoole takes a fundamentally different approach than ReactPHP: it is a PHP extension written in C that provides a complete, long-running server process, rather than being a pure library. A Swoole server starts a master process, which in turn manages several worker processes. Each worker can handle thousands of concurrent requests through coroutines, without needing a separate operating system process or thread for every request. This drastically reduces memory and context-switching overhead compared to the classic PHP-FPM model with one process per request.

Coroutines are lightweight, cooperative execution units within a single PHP process. As soon as a coroutine hits an I/O operation, for example a database query or an HTTP call through a Swoole-compatible client library, it automatically returns control to the coroutine scheduler, which runs another ready coroutine. Once the I/O result is available, the original coroutine resumes exactly at the point where it paused. From a developer's perspective, the code looks almost like classic, synchronous PHP, even though fully asynchronous PHP with non-blocking I/O runs underneath, a major advantage over explicit callback chaining.

For this mechanism to work, I/O calls must be coroutine-aware. Swoole offers so-called runtime hooking (Swoole\Runtime::enableCoroutine()) for this, which transparently replaces standard functions such as PDO, curl, fopen or Redis with coroutine-safe implementations. Without this hooking, a classic, synchronous call inside a coroutine blocks the entire worker process, and every other coroutine in that worker has to wait, a detail examined in more depth in the pitfalls section below.


<?php
declare(strict_types=1);

use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;

// Swoole HTTP server, one process handles thousands of coroutines
$server = new Server('0.0.0.0', 9501);

$server->set([
    'worker_num' => 4,       // number of worker processes
    'enable_coroutine' => true,
]);

$server->on('request', function (Request $request, Response $response): void {
    // Each request runs inside its own coroutine automatically
    $userId = (int) ($request->get['id'] ?? 0);

    // With runtime hooking enabled, this PDO call yields the
    // coroutine instead of blocking the worker process
    $pdo = new PDO('mysql:host=db;dbname=shop', 'app', 'secret');
    $stmt = $pdo->prepare('SELECT name FROM customer WHERE id = ?');
    $stmt->execute([$userId]);
    $row = $stmt->fetch();

    $response->header('Content-Type', 'application/json');
    $response->end(json_encode($row ?: ['error' => 'not found']));
});

$server->start();

5. Typical use cases for asynchronous PHP

WebSocket servers are the most obvious use case for asynchronous PHP, because by definition they require permanently open connections, for which the classic PHP-FPM model with one worker per connection is not economically viable. Chat applications, live notifications, collaborative editors or price tickers in e-commerce systems can be run with Swoole or ReactPHP as a standalone process alongside the existing application, managing thousands of concurrent connections with manageable resource usage.

Queue workers also benefit strongly from asynchronous PHP, especially when a worker addresses many external services in parallel, for example when sending notifications across multiple channels at once or matching against several APIs per job. Instead of processing each channel sequentially and blocking, coroutines or promises allow multiple I/O operations to be triggered in parallel and evaluated together, which significantly reduces the total runtime per job. Microservices that primarily act as I/O mediators between several backend systems, for example an aggregation layer that combines data from multiple APIs, also benefit from the high concurrency that asynchronous PHP enables without additional processes.

Long polling, where an HTTP connection is deliberately kept open until new data is available or a timeout is reached, is another classic case. In the PHP-FPM model, every long-polling connection would block a worker for the entire wait time, which hard-limits the maximum number of concurrent clients to the worker count. With asynchronous PHP, a waiting connection costs practically no CPU time as long as no event occurs, allowing the same hardware to support a multiple of the concurrent wait states.


<?php
declare(strict_types=1);

use Swoole\WebSocket\Server;
use Swoole\Http\Request;

// Minimal WebSocket server broadcasting messages to all clients
$server = new Server('0.0.0.0', 9502);

$server->on('open', function (Server $server, Request $request): void {
    echo "Client {$request->fd} connected\n";
});

$server->on('message', function (Server $server, $frame): void {
    // Broadcast the incoming message to every connected client
    foreach ($server->connections as $fd) {
        if ($server->isEstablished($fd)) {
            $server->push($fd, $frame->data);
        }
    }
});

$server->on('close', function (Server $server, int $fd): void {
    echo "Client {$fd} disconnected\n";
});

$server->start();

6. Pitfalls: global state, memory leaks and blocking calls

The most serious pitfall with asynchronous PHP in Swoole is using global or static variables to hold state. Unlike PHP-FPM, where every request starts in a fresh process and memory is fully released afterwards, a Swoole worker keeps running through thousands of requests without PHP ever being reinitialized in between. Static class variables or global variables that unintentionally share state between requests lead to bugs where one user's data suddenly appears in another user's request, a classic issue from the long-running-process world that many PHP developers coming from the classic request-response model have never encountered.

Closely related is the problem of memory leaks: since the process is not terminated after each request, memory that is not correctly released accumulates over the worker's lifetime. Typical causes are growing arrays in static properties, unclosed database connections, or event listeners that get registered again on every request but are never removed. Swoole does offer the configuration option max_request, which automatically restarts a worker after a certain number of requests, but that is a safety net, not a substitute for clean resource management in the code itself.

The third, and often the most surprising pitfall for newcomers, concerns blocking calls inside coroutines. A classic sleep() call or a PDO call without enabled runtime hooking blocks the entire worker process, not just the current coroutine. Since a worker serves hundreds of coroutines at once, a single blocking call freezes the processing of every other request in that worker, an effect that often only shows up under high concurrency in load tests and is then hard to diagnose.


<?php
declare(strict_types=1);

use Swoole\Coroutine;

Coroutine\run(function (): void {

    // WRONG: blocking sleep() freezes the entire worker process,
    // every other coroutine in this worker has to wait too
    Coroutine::create(function (): void {
        sleep(3); // blocks the OS thread, not coroutine-aware
        echo "Task A done\n";
    });

    // RIGHT: Coroutine::sleep() yields control back to the
    // scheduler, other coroutines keep running in the meantime
    Coroutine::create(function (): void {
        Coroutine::sleep(3.0); // coroutine-safe, non-blocking
        echo "Task B done\n";
    });

    // WRONG: PDO without runtime hooking blocks the worker
    // during the entire query execution time
    Coroutine::create(function (): void {
        $pdo = new PDO('mysql:host=db;dbname=shop', 'app', 'secret');
        $pdo->query('SELECT SLEEP(2)'); // blocks, no coroutine yield
    });

    // RIGHT: enable coroutine hooking once at bootstrap so PDO,
    // curl, streams and Redis become coroutine-safe automatically
    // Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
});

7. Swoole vs. ReactPHP head to head

Swoole and ReactPHP solve the same underlying problem, avoiding blocking I/O in PHP, with fundamentally different architectural decisions. Swoole replaces the entire server process and brings coroutines close to a language feature, while ReactPHP remains a pure library that relies on explicit promises and can be installed in any existing PHP environment. This decision has direct consequences for performance, learning curve and how easily asynchronous PHP can be integrated into an existing project.

Criterion Swoole ReactPHP
Architecture model C extension with its own server process and coroutine scheduler Pure PHP library, event loop plus promises, no dedicated process type
Performance under high concurrency Very high, native coroutines with minimal overhead per connection Good, but noticeably slower than Swoole due to pure PHP and promise overhead
Learning curve Medium to high: coroutine semantics, hooking and worker configuration must be understood Lower for PHP developers: promises and callbacks instead of a new execution model
Ecosystem and extensions Own Swoole-compatible clients for MySQL, Redis, HTTP, WebSocket, gRPC Modular packages for HTTP, DNS, processes, streams, broad Composer integration
Compatibility with existing code Requires runtime hooking, classic blocking libraries are often problematic High: runs alongside existing PHP-FPM code, no extension installation needed
Use in production Widely used for high-load APIs, WebSocket servers and microservices, especially in Asia and internationally Widely used for lightweight async tasks, CLI tools and integrations without an extension requirement

In practice, infrastructure often decides the choice: if an extension is allowed to be installed and maximum performance under very high concurrency is required, Swoole is the more consistent choice. If asynchronous PHP is only needed selectively for one part of an application and there is no control over the server environment, for example in shared hosting environments, ReactPHP is often the more pragmatic option, because it can be installed as a plain Composer dependency.

8. Integrating asynchronous PHP step by step into existing applications

A complete rewrite of an existing PHP-FPM application to asynchronous PHP is rarely sensible, or even necessary. The pragmatic approach is to introduce asynchronous PHP specifically for the parts that genuinely benefit from high concurrency or long-lived connections, while the rest of the application keeps running unchanged on PHP-FPM. A WebSocket server for live notifications, for example, can be run as a separate Swoole or ReactPHP process alongside the existing web application, communicating with the classic PHP-FPM part via a message queue or an internal API.

This separation has several advantages: the risk to the existing application stays minimal, because the new asynchronous process runs in isolation and can be restarted independently in case of problems, without affecting the rest of the application. Development teams can gradually get familiar with the particularities of asynchronous PHP on a clearly bounded subsystem, instead of converting the entire codebase at once. At the same time, the success of the new approach can be measured against concrete metrics, such as the number of concurrently held connections or the latency of push notifications, before a decision is made about a broader rollout.

When integrating, it is important to draw clear boundaries between the two worlds: the asynchronous process should not import shared PHP classes with hidden blocking I/O that were written for the PHP-FPM context, without checking them for coroutine safety. Configuration values, sessions and state should be shared through external systems like Redis or a database, rather than through shared PHP process memory, because the two process worlds run and scale independently of each other. This keeps asynchronous PHP a complementary tool for specific problems, rather than a risky fundamental decision for the entire system.

9. Setting realistic performance expectations

Asynchronous PHP delivers its biggest advantage for I/O-bound workloads, meaning tasks where most of the time is spent waiting on network, database or filesystem rather than doing actual CPU computation. An endpoint that queries several external APIs in parallel and merges the results benefits massively from coroutines or promises, because the wait times overlap instead of adding up. A single request that waits on three APIs with 200 milliseconds response time each takes roughly 200 milliseconds with parallelization, instead of 600 milliseconds in the sequential, blocking model.

For CPU-bound workloads, such as complex calculations, image processing or large data aggregations without significant I/O wait, asynchronous PHP on the other hand brings little to no advantage, because an event loop still runs single-threaded. A computationally heavy coroutine blocks the event loop for every other coroutine in the same worker just as much as a blocking I/O call without hooking would. For such cases, real parallelism across multiple processes is still needed, for example through additional Swoole workers or offloaded queue jobs, not through coroutines within a single process.

Realistically, switching to asynchronous PHP pays off when the number of concurrent connections or the I/O wait time per request is significant enough that classic PHP-FPM hits memory or worker limits. For most classic CRUD web applications with manageable concurrent load, PHP-FPM still delivers sufficient performance at considerably lower complexity. The switch should therefore always be justified by concrete numbers, measured latencies, worker utilization, number of concurrent connections, rather than by the assumption that asynchronous PHP is inherently faster.

10. Summary

Asynchronous PHP with Swoole or ReactPHP solves a specific problem of the classic PHP-FPM model: the inefficient binding of an entire worker process to a single, often waiting connection. Through event loops, non-blocking I/O and, in Swoole's case, coroutines, thousands of concurrent connections can be managed in a single process, without blocking a full process on every I/O operation. ReactPHP offers the more pragmatic entry point as a pure library, Swoole the higher performance and coroutine proximity as a full-fledged server process.

Success with asynchronous PHP depends decisively on knowing the typical pitfalls: global state in long-running processes, memory leaks over a worker's lifetime, and blocking calls inside coroutines without runtime hooking. Anyone who introduces asynchronous PHP step by step and specifically for I/O-heavy subproblems, instead of rewriting the entire application, gets the benefits of high concurrency without taking on the risk of a risky, complete rewrite.

Asynchronous PHP with Swoole and ReactPHP - The Key Takeaways

Core problem

PHP-FPM blocks an entire worker per request. Asynchronous PHP manages thousands of connections in a single process through an event loop.

Swoole vs. ReactPHP

Swoole: C extension, dedicated server process, coroutines, highest performance. ReactPHP: pure library, promises, simpler integration.

Typical pitfalls

Global state persisting across requests, memory leaks in long-running processes, blocking calls in coroutines without hooking.

When it pays off

For I/O-bound workloads with high concurrency, WebSockets, queue workers. For CPU-bound tasks it brings barely any benefit.

11. FAQ: Asynchronous PHP with Swoole and ReactPHP

1What exactly does asynchronous PHP mean?
An execution model without blocking I/O operations. Instead of waiting for an answer, the code registers a callback, and the event loop handles other tasks in the meantime.
2A replacement for PHP-FPM?
No, a complement for WebSockets or queue workers with heavy I/O load. For classic web requests, PHP-FPM usually remains simpler and sufficient.
3Swoole vs. ReactPHP?
Swoole: C extension, dedicated server process, native coroutines. ReactPHP: pure PHP library with promises, usable via Composer without an extension requirement.
4What are coroutines?
Lightweight, cooperative execution units within a process. On I/O wait, they automatically return control to the scheduler.
5Why does PDO still block?
Without runtime hooking, PDO is not coroutine-aware and blocks the entire worker. Swoole\Runtime::enableCoroutine() transparently replaces PDO with a coroutine-safe variant.
6Do I have to rewrite everything?
No, a separate process for a bounded subsystem, such as a WebSocket server, alongside the existing PHP-FPM application is the pragmatic approach.
7Benefit for CPU load?
Barely. The event loop is single-threaded, a compute-heavy coroutine blocks every other one in the same worker. CPU-bound tasks need real process parallelism.
8Typical bugs with Swoole?
Global or static state unintentionally shared between requests, plus memory leaks from resources not released over the worker's lifetime.
9Best use cases?
WebSocket servers, long polling, queue workers with many parallel API calls, and microservices aggregating data from multiple backends.
10Is Swoole production stable?
Yes, used internationally in high-load systems. What matters is accounting for the pitfalls around coroutines and runtime hooking from the start.

Mironsoft

PHP architecture, performance engineering and Magento development

Too many blocking processes in your PHP application?

We analyze where asynchronous PHP with Swoole or ReactPHP actually adds value, and integrate it selectively alongside your existing PHP-FPM or Magento infrastructure, instead of proposing a risky full rewrite.

Architecture review

Assessing whether Swoole, ReactPHP or classic PHP-FPM is the right choice for your use case

WebSocket & workers

Building isolated asynchronous processes for live features and queue workers alongside existing code

Performance audit

Measuring I/O wait times and worker utilization as a decision basis before any migration