Skip to main content
  1. Languages/
  2. PHP Guides/

Mastering PHP Concurrency: A Deep Dive into ReactPHP vs. Swoole

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

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" -n

We 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.

sequenceDiagram autonumber participant Client participant SyncPHP as PHP (FPM) participant AsyncPHP as PHP (Async/EventLoop) participant DB as Database note over SyncPHP: TRADITIONAL (Blocking) Client->>SyncPHP: Request A SyncPHP->>DB: Query A (Wait...) note right of SyncPHP: Process BLOCKED DB-->>SyncPHP: Result A SyncPHP-->>Client: Response A note over AsyncPHP: ASYNCHRONOUS (Non-Blocking) Client->>AsyncPHP: Request A Client->>AsyncPHP: Request B AsyncPHP->>DB: Query A (Don't Wait) AsyncPHP->>DB: Query B (Don't Wait) note right of AsyncPHP: Process Handles other events DB-->>AsyncPHP: Result A Ready AsyncPHP-->>Client: Response A DB-->>AsyncPHP: Result B Ready AsyncPHP-->>Client: Response B

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/http

The 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:

  1. Swoole saves the current stack (variables, pointer).
  2. It marks this request as “waiting.”
  3. The Worker process immediately grabs the next incoming HTTP request.
  4. 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
#

graph TD subgraph ReactPHP_Architecture R_Loop[Event Loop] R_Stream[Stream Select] R_Task[Task/Promise] R_Loop --> R_Stream R_Stream --> R_Task R_Task -->|Callback| R_Loop end subgraph Swoole_Architecture S_Master[Master Process] S_Reactor[Reactor Threads] S_Worker[Worker Processes] S_Co[Coroutines] S_Master --> S_Reactor S_Reactor -->|Distribute| S_Worker S_Worker --> S_Co S_Co -.->|Yield I/O| S_Worker end

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 WeakReference for 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 Context objects (Swoole has Swoole\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:

  1. 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.
  2. 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:

  1. Install the php-concurrency-lab environment we set up.
  2. Try converting a slow API endpoint in your current project to a standalone Swoole microservice.
  3. Benchmark it using wrk or ab.

Concurrency is a powerful tool in your arsenal. Use it wisely, watch out for memory leaks, and enjoy the speed!


Further Reading
#