0

Swoole and OpenSwoole — coroutine-based PHP server

Advanced5 min read·php-11-007
compare

Concept

Swoole is a C extension for PHP that transforms it into a high-performance asynchronous framework. Instead of the traditional Apache/Nginx + FPM setup, Swoole runs PHP as a long-lived server process — similar to Node.js or Go's net/http server. OpenSwoole is a community fork of Swoole with a different governance model and some API differences; for most purposes the concepts are identical.

The core architecture is multi-process with a coroutine scheduler in each worker. Swoole spawns a master process, a manager process, and N worker processes. The master process accepts incoming connections using an event loop and dispatches them to workers. Each worker runs its own coroutine scheduler, enabling thousands of concurrent coroutines per worker. Combined, a single server with 8 workers can handle tens of thousands of concurrent connections.

Swoole\Runtime::enableCoroutineHook() (also Co\run() in newer versions) monkey-patches PHP's blocking I/O functions at the C extension level. After calling this, standard PHP functions like mysqli_query(), curl_exec(), usleep(), file_get_contents() (for local files), and even pdo->query() become coroutine-aware: they internally suspend the current coroutine and yield to the scheduler while waiting for I/O, resuming when the result is ready. This is the killer feature: existing code becomes async without rewriting.

OpenSwoole diverges by having a cleaner hook API (OpenSwoole\Runtime::enableCoroutineHook(SWOOLE_HOOK_ALL)) and slightly different defaults. Both extensions provide Coroutine::create(), Co::go(), channels (Swoole\Coroutine\Channel), WaitGroup primitives, and coroutine-local storage.

The deployment model differs fundamentally from FPM: the PHP process never dies between requests. Application bootstrap (service container construction, config loading, route registration) happens once at startup. This is a 10–100x speedup for frameworks like Laravel — but it means any static state, file handles, or mutable class properties that survive a request handler will leak into the next request.

FeatureSwooleOpenSwoole
OriginHan Tianfeng (2012)Fork of Swoole
LicenseApache 2.0Apache 2.0
Coroutine hookSwoole\Runtime::enableCoroutineHookOpenSwoole\Runtime::enableCoroutineHook
HTTP serverSwoole\Http\ServerOpenSwoole\Http\Server
Release cadenceSlowerFaster
Laravel Octane supportYesYes

Code Example

php
<?php
declare(strict_types=1);

/**
 * Swoole HTTP server with coroutine-based concurrent MySQL queries.
 *
 * Install: pecl install swoole
 * Run:     php server.php
 */

use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Coroutine;
use Swoole\Coroutine\MySQL;
use Swoole\Coroutine\WaitGroup;

// Enable coroutine hooks for all blocking I/O BEFORE starting the server.
\Swoole\Runtime::enableCoroutineHook(SWOOLE_HOOK_ALL);

$server = new Server('0.0.0.0', 8080, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);

$server->set([
    'worker_num'    => swoole_cpu_num(), // One worker per CPU core.
    'max_coroutine' => 100_000,           // Max concurrent coroutines per worker.
    'hook_flags'    => SWOOLE_HOOK_ALL,
]);

$server->on('Request', function (Request $request, Response $response): void {
    // Each HTTP request runs inside its own coroutine automatically.

    $wg = new WaitGroup();
    $userResult   = null;
    $configResult = null;

    // Run two DB queries concurrently inside the same request coroutine.
    $wg->add(2);

    Coroutine::create(function () use ($wg, &$userResult): void {
        $db = new MySQL();
        $db->connect([
            'host'     => '127.0.0.1',
            'port'     => 3306,
            'user'     => 'root',
            'password' => 'secret',
            'database' => 'app',
        ]);
        // This suspends the coroutine while waiting for MySQL — does NOT block the worker.
        $userResult = $db->query('SELECT * FROM users LIMIT 1');
        $wg->done();
    });

    Coroutine::create(function () use ($wg, &$configResult): void {
        $db = new MySQL();
        $db->connect([
            'host' => '127.0.0.1', 'port' => 3306,
            'user' => 'root', 'password' => 'secret', 'database' => 'app',
        ]);
        $configResult = $db->query('SELECT * FROM settings LIMIT 1');
        $wg->done();
    });

    // Suspends this coroutine until both child coroutines call done().
    $wg->wait(3.0); // 3-second timeout.

    $response->header('Content-Type', 'application/json');
    $response->end(json_encode([
        'user'   => $userResult[0] ?? null,
        'config' => $configResult[0] ?? null,
    ]));
});

// -------------------------------------------------------
// Coroutine channel — producer/consumer pattern
// -------------------------------------------------------
Coroutine\run(function (): void {
    $channel = new Coroutine\Channel(10); // Buffered channel, capacity 10.

    // Producer coroutine.
    Coroutine::create(function () use ($channel): void {
        for ($i = 1; $i <= 5; $i++) {
            $channel->push("job-{$i}");
            Coroutine::sleep(0.1);
        }
        $channel->close();
    });

    // Consumer coroutine.
    Coroutine::create(function () use ($channel): void {
        while (true) {
            $job = $channel->pop(1.0); // Wait up to 1 s for a job.
            if ($job === false) {
                break; // Channel closed.
            }
            echo "Processing: {$job}\n";
        }
    });
});

$server->start();

Interview Q&A

Q: How does Swoole's coroutine hook make existing blocking PHP code work asynchronously?

Swoole implements coroutine hooks at the C extension level by replacing the underlying C function pointers for PHP's I/O operations (network syscalls like connect(), read(), write(), and sleep functions). When a coroutine calls mysqli_query(), Swoole's hook intercepts it, registers the underlying file descriptor with the event loop, suspends the current coroutine via its scheduler (analogous to Fiber::suspend()), and returns control to the scheduler. When the kernel signals that data is available on that descriptor, the event loop resumes the suspended coroutine. The PHP code sees a normal synchronous return from mysqli_query() — it is unaware that it was suspended. This is "transparent async" and is the most powerful feature distinguishing Swoole from ReactPHP or Amp, which require all I/O libraries to be written with explicit async primitives.


Q: What are the most common sources of state leakage between requests in a Swoole/Octane long-lived process?

The main sources are: (1) Static properties on classes — if middleware sets User::$current = $user and the property is never cleared, the next request inherits it. (2) Singleton services registered in the IoC container that hold mutable state, like an AuthManager that caches the authenticated user. (3) Global variables ($_SESSION, $_SERVER overwritten by middleware). (4) PDO connection objects stored in static properties that silently share a transaction across requests. Laravel Octane mitigates this by flushing the container's request-scoped bindings and resetting certain facades between requests, documented in its flush and warm configuration. The pattern is to treat everything that was mutable-between-requests in FPM as potentially shared in Octane.


Q: What is a Swoole Coroutine\Channel and how does it differ from PHP's SplQueue?

Swoole\Coroutine\Channel is a coroutine-safe, optionally buffered FIFO queue for passing values between coroutines. Its push() and pop() operations are coroutine-aware: if you pop() from an empty buffered channel, the coroutine suspends until another coroutine pushes a value (or the timeout expires). If you push() to a full channel, the pushing coroutine suspends until a consumer pops. This is exactly Go's buffered channel semantics. SplQueue is a plain data structure with no concurrency awareness — calling dequeue() on an empty SplQueue throws an exception immediately; it cannot suspend a coroutine. In Swoole, channels are the idiomatic way to implement producer/consumer pipelines, work pools, and rate-limited concurrency.