For nearly two decades, the standard mental model for PHP execution has been straightforward: One Request, One Process.
The browser sends a request, Apache or Nginx hands it to PHP-FPM, the script runs from top to bottom, maybe hits a database, renders HTML, and dies. It’s a “shared-nothing” architecture. It’s predictable. It’s stable.
It is also increasingly inefficient for modern workloads.
As we step into 2026, the demand for real-time capabilities—WebSockets, microservices with high I/O throughput, and event-driven architectures—has pushed the traditional LAMP stack to its limits. If your application needs to handle 10,000 concurrent connections, spawning 10,000 PHP-FPM worker processes isn’t just inefficient; it will crash your server.
This is where Asynchronous PHP comes in.
In this deep dive, we are going to tear apart the two titans of the PHP concurrency world: ReactPHP and Swoole. We will move beyond the “Hello World” examples and build robust, non-blocking applications. We’ll explore the Event Loop, Coroutines, and the architectural shifts required to write high-performance PHP today.
Prerequisites and Environment Setup #
Before we write a single line of code, we need to ensure our environment is ready for asynchronous operations. Unlike standard PHP scripts, these applications run as persistent processes (daemons).
Requirements #
- PHP 8.3+: We are using modern syntax (Enums, Readonly classes, Types).
- Composer: For dependency management.
- Linux/macOS: Swoole is primarily designed for *nix systems. If you are on Windows, you must use WSL2 or Docker.
- Docker (Optional but Recommended): To isolate the Swoole extension environment.
Setting Up the Project #
Let’s create a directory for our experiments.
mkdir php-concurrency-lab
cd php-concurrency-lab
composer init --name="phpdevpro/concurrency-lab" --require="php:^8.3" -nWe will install dependencies as we progress through the article.
Part 1: The Paradigm Shift #
To understand ReactPHP and Swoole, you must unlearn the synchronous flow. In standard PHP, if you query a database, the CPU sits idle waiting for MySQL to reply. This is Blocking I/O.
In Non-Blocking I/O, we initiate the query and immediately move to the next task. When MySQL replies, we handle the result.
Visualizing the Difference #
Let’s look at how a traditional Request/Response cycle compares to an Event-Driven cycle using Mermaid.
In the second flow, a single PHP process handles multiple requests simultaneously by interleaving I/O operations.
Part 2: ReactPHP - Pure PHP Power #
ReactPHP is one of the oldest and most mature ecosystems for async PHP. Its biggest selling point? It requires no special C-extensions. It runs on standard PHP.
It implements the Reactor Pattern. It revolves around a single event loop that monitors file descriptors (network sockets, files) and timers.
Installation #
composer require react/event-loop react/httpThe Event Loop #
The core of ReactPHP is the Loop. Think of the loop as an infinite while(true) that checks: “Do I have any timers due? Do I have any incoming data on this socket?”
Here is a basic example illustrating timers, which proves the code doesn’t block.
<?php
// react-timer.php
require 'vendor/autoload.php';
use React\EventLoop\Loop;
echo "Script starting...\n";
// This runs immediately
Loop::addTimer(1.0, function () {
echo "[1 second] Single shot timer finished.\n";
});
// This repeats
Loop::addPeriodicTimer(2.0, function () {
echo "[2 seconds] Periodic tick.\n";
});
// This simulates a heavy task but strictly using timers
Loop::addTimer(0.5, function () {
echo "[0.5 seconds] Fast timer.\n";
});
echo "Loop is about to run. The script will NOT exit immediately.\n";
// This hands control to the Event Loop
// The script effectively pauses here and processes events
Loop::run();Output:
Script starting...
Loop is about to run. The script will NOT exit immediately.
[0.5 seconds] Fast timer.
[1 second] Single shot timer finished.
[2 seconds] Periodic tick.
[2 seconds] Periodic tick.
... (Ctrl+C to stop)Building a Non-Blocking HTTP Server #
Now, let’s build something real. A simple HTTP server that simulates a slow operation without blocking other requests.
<?php
// react-server.php
require 'vendor/autoload.php';
use React\Http\HttpServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Loop;
$server = new HttpServer(function (ServerRequestInterface $request) {
// Simulate a slow database call using a Promise
return new React\Promise\Promise(function ($resolve) {
// Wait 1.5 seconds, then send response
Loop::addTimer(1.5, function () use ($resolve) {
$resolve(new Response(
200,
['Content-Type' => 'application/json'],
json_encode(['message' => 'Hello from ReactPHP', 'time' => time()])
));
});
});
});
$socket = new React\Socket\SocketServer('0.0.0.0:8080');
$server->listen($socket);
echo "ReactPHP Server running at http://127.0.0.1:8080\n";Key Takeaway: Notice we return a Promise. In ReactPHP, you cannot use sleep(1). If you use sleep(1), the entire server pauses for everyone. You must use Loop::addTimer or async libraries that return Promises.
Part 3: Swoole - The Coroutine Beast #
Swoole takes a different approach. It is a PECL extension written in C. It overrides low-level PHP behaviors to introduce Coroutines.
Swoole is generally faster than ReactPHP and provides a programming style that looks synchronous (linear) but executes asynchronously. This is similar to Go (Golang) or modern JavaScript async/await.
Installation #
You need the Swoole extension.
Using PECL:
pecl install swoole(Add extension=swoole.so to your php.ini)
Using Docker (Recommended for this demo):
Create a Dockerfile:
FROM php:8.3-cli
RUN pecl install swoole && docker-php-ext-enable swoole
WORKDIR /app
COPY . .
CMD ["php", "swoole-server.php"]The Coroutine Model #
In ReactPHP, we used callbacks and Promises. In Swoole, we write linear code, and the Swoole engine handles the context switching automatically when it detects I/O.
Let’s rewrite the HTTP server in Swoole.
<?php
// swoole-server.php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
// 1. Create the Server
$http = new Server("0.0.0.0", 9501);
// 2. Configure settings (Workers, Daemonize, etc.)
$http->set([
'worker_num' => 4, // 4 Worker processes
'enable_coroutine' => true,
]);
// 3. Define the Request Handler
$http->on("request", function (Request $request, Response $response) {
// This looks like blocking code, right?
// In standard PHP, sleep(1) stops the process.
// In Swoole, Co::sleep(1) yields the coroutine, letting the worker handle other requests.
\Swoole\Coroutine::sleep(1.5);
$response->header("Content-Type", "application/json");
$response->end(json_encode([
"message" => "Hello from Swoole",
"time" => time()
]));
});
echo "Swoole HTTP Server running at http://127.0.0.1:9501\n";
$http->start();Why Swoole feels “Magic” #
When \Swoole\Coroutine::sleep(1.5) is called:
- Swoole saves the current stack (variables, pointer).
- It marks this request as “waiting.”
- The Worker process immediately grabs the next incoming HTTP request.
- After 1.5s, Swoole restores the stack and resumes execution on the line after
sleep.
This requires significantly less mental overhead than managing Promise chains in ReactPHP.
Part 4: Comparative Analysis #
Let’s break down the differences. This is crucial for choosing the right tool for your production environment.
| Feature | ReactPHP | Swoole |
|---|---|---|
| Type | Native PHP Library | C Extension (PECL) |
| Architecture | Event Loop (Reactor) | Coroutines + Event Loop + Workers |
| Coding Style | Promises / Callbacks (JS style) | Synchronous-looking (Go style) |
| Performance | High | Very High (Native C speed) |
| Installation | composer require (Easy) |
Requires compiling extension / Docker |
| Ecosystem | Modular (Components for everything) | All-in-one (HTTP, WebSocket, Redis, SQL) |
| Context Switching | User-land (Slower) | C-level (Faster) |
| Dev Experience | Steeper learning curve (Promises) | Easier transition for sync PHP devs |
Conceptual Architecture Diagram #
Part 5: Real-World Scenario: Parallel Data Processing #
Let’s look at a scenario where we need to fetch data from 3 different URLs.
- Sync PHP: 1s + 1s + 1s = 3 seconds.
- Async PHP: max(1s, 1s, 1s) ≈ 1 second.
Swoole Implementation (The “Go” Channel Way) #
Swoole allows us to use Channels to communicate between coroutines, ensuring we gather all results.
<?php
// swoole-channels.php
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use function Swoole\Coroutine\run;
use function Swoole\Coroutine\go;
// We use the 'run' function to create a coroutine container
run(function() {
$chan = new Channel(3); // Create a channel with capacity 3
$start = microtime(true);
$urls = [
'https://www.google.com',
'https://www.php.net',
'https://github.com'
];
foreach ($urls as $url) {
// Spawn a lightweight thread (coroutine) for each URL
go(function() use ($chan, $url) {
// Simulate network latency
$latency = rand(500, 1000) / 1000;
Coroutine::sleep($latency);
// In a real app, you would use Swoole's curl hook or HTTP client here
$chan->push("Fetched $url in {$latency}s");
});
}
$results = [];
for ($i = 0; $i < 3; $i++) {
// Pop data from channel (this suspends until data is available)
$results[] = $chan->pop();
}
$end = microtime(true);
echo "Total Time: " . round($end - $start, 4) . " seconds\n";
print_r($results);
});Output:
Total Time: 1.0023 seconds
Array
(
[0] => Fetched https://www.php.net in 0.6s
[1] => Fetched https://www.google.com in 0.8s
[2] => Fetched https://github.com in 1.0s
)We processed three tasks in the time it took to do the longest one.
Part 6: Best Practices and Common Pitfalls #
Moving from the “Stateless” world of FPM to the “Stateful” world of Daemon processes requires a mindset shift.
1. Memory Leaks are Fatal #
In PHP-FPM, if your script leaks 1MB of RAM, it doesn’t matter; the process dies after the request. In React/Swoole, the process runs for days. A 1MB leak per request will crash your server in minutes.
Solution:
- Unset large variables explicitly.
- Use
WeakReferencefor caching. - Monitor memory usage using tools like Prometheus.
2. The Singleton Trap #
In FPM, static $db is fine because it’s isolated to one request. In Swoole/ReactPHP, a static variable is shared across all requests handled by that worker.
Bad Code:
class Database {
public static $connection; // DANGEROUS IN ASYNC
}If Request A sets user context on this connection, Request B might accidentally execute a query as that user.
Solution:
- Inject dependencies via constructor.
- Use
Contextobjects (Swoole hasSwoole\Coroutine::getContext()).
3. Database Connections #
You cannot reuse a single PDO instance for concurrent coroutines. If Coroutine A starts a transaction, and Coroutine B tries to query, it will crash or pollute the transaction.
Solution:
- You must use a Connection Pool.
- Swoole has a built-in Database Pool.
- ReactPHP typically manages this via libraries like
react/mysql.
4. Blocking the Loop (The Cardinal Sin) #
Never use sleep(), file_get_contents(), or standard PDO (unless configured with hooks in Swoole) inside the loop.
Example of what NOT to do in ReactPHP:
$server->on('request', function ($req) {
// This stops the ENTIRE server processing
// No one else can connect while this file is being read
$data = file_get_contents('large-file.txt');
return new Response(200, [], $data);
});Part 7: Performance Considerations #
While benchmarks vary based on hardware, generally speaking:
-
Raw Throughput (Requests per Second):
- Swoole > ReactPHP > PHP-FPM
- Swoole can often handle 50k-100k req/sec on decent hardware.
- ReactPHP performs admirably, often hitting 20k-40k req/sec.
- PHP-FPM typically caps around 2k-5k req/sec without massive horizontal scaling.
-
CPU Usage:
- Async solutions use less CPU for I/O bound tasks because they don’t block.
- However, the PHP code itself (business logic) is still single-threaded in the loop (unless using Swoole Tasks).
When to use what?
- Choose ReactPHP if: You want to add async features (like WebSockets) to an existing Laravel/Symfony app without changing your server infrastructure or installing C extensions.
- Choose Swoole (or OpenSwoole) if: You are building a high-performance microservice, a game server, or a system that needs the absolute maximum throughput and you control the environment (Docker).
Conclusion #
The era of “PHP is just for simple websites” is long gone. With ReactPHP and Swoole, PHP stands toe-to-toe with Node.js and Go in the realm of high-concurrency applications.
ReactPHP offers a pure PHP, ecosystem-friendly entry point into event-driven programming. Swoole offers raw power and a synchronous coding style powered by coroutines, bridging the gap between ease of use and extreme performance.
Action Plan for You:
- Install the
php-concurrency-labenvironment we set up. - Try converting a slow API endpoint in your current project to a standalone Swoole microservice.
- Benchmark it using
wrkorab.
Concurrency is a powerful tool in your arsenal. Use it wisely, watch out for memory leaks, and enjoy the speed!
Further Reading #
- ReactPHP Official Documentation
- Swoole Documentation
- PHP 8.4 Fibers - The future of native PHP async.
- [Frameworks on Async]: Check out Hyperf (based on Swoole) or ReactPHP with Laravel.