My thanks to Edmond for his work on PHP async I/O. Regardless of how the vote
turns out, I believe everything is moving in a positive direction. This will
undoubtedly leave a bold mark in the history of PHP’s evolution.
I’ll share some information and thoughts to help everyone understand async.
These may include views on PHP Fiber, amphp, reactphp, and FrankenPHP, but
please remember they are purely technical reflections, with no praise or
criticism implied.
1. What lies at the core of Swoole’s async design
Using Boost.Context assembly to implement C/C++ stackful coroutines is no
longer esoteric; PHP Fiber and Swoole are almost identical in their low-level
principles. The only difference is that Swoole suspends and resumes coroutines
entirely in C/C++, whereas PHP Fiber does the opposite—suspension happens in
PHP code. While PHP also exposes relevant APIs, they are rarely used in Swoole.
Because both the C stack and the PHP stack are fully preserved, this approach
is actually very safe and won’t cause memory errors—unless static or global
memory is misused. Swoole runs over 1,700 tests on GitHub Actions, many of
which involve multiple coroutines issuing concurrent requests. Before testing,
containers spin up mysql, pgsql, oracle, redis, firebirdsql, httpbin,
tinyproxy, pure-ftpd, and many other databases and servers to interact with
code in phpt files. The breadth of these tests speaks to its reliability.
Unlike amphp/reactphp, Swoole does not invent new APIs; it reuses PHP’s
existing functions. Swoole hooks into PHP streams, the standard library, and
other extension functions—such as sleep, stream_socket_client,
stream_socket_server, file_get_contents, fsockopen, curl_*, mysqli, pdo_mysql.
Inside a Swoole coroutine, these calls are no longer synchronous blocking I/O;
they become non-blocking. When I/O isn’t ready, the runtime suspends the
current coroutine and uses epoll to watch for readable events, resuming the
coroutine only when the operation completes.
An example:
Co\run(function() {
Co\go(function() {
while(1) {
sleep(1);
$fp =
stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
echo fread($fp, 8192), PHP_EOL;
}
});
Co\go(function() {
$fp = stream_socket_server("tcp://0.0.0.0:8000",
$errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
while(1) {
$conn = stream_socket_accept($fp);
fwrite($conn, 'The local time is ' .
date('n/j/Y g:i a'));
}
});
Co\go(function() {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
while(true) {
$redis->subscribe(['test'],
function ($instance, $channelName, $message) {
echo 'New redis
message: '.$channelName, "==>", $message, PHP_EOL;
});
}
});
Co\go(function() {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$count = 0;
while(true) {
sleep(2);
$redis->publish('test','hello,
world, count='.$count++);
}
});
});
By conventional understanding, this code shouldn’t run: every function that
performs network I/O would block the entire process. But in the Swoole
environment, the program runs smoothly. We can even modify the code to increase
the number of clients by several thousand, and it still runs stably.
Co\run(function() {
Co\go(function() {
while(1) {
sleep(1);
$fp =
stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
echo fread($fp, 8192), PHP_EOL;
}
});
$n = 2000;
while($n--) {
Co\go(function() {
$fp =
stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr, STREAM_SERVER_BIND
| STREAM_SERVER_LISTEN);
while(1) {
$conn =
stream_socket_accept($fp);
fwrite($conn, 'The
local time is ' . date('n/j/Y g:i a'));
}
});
}
});
Swoole’s aim is to leverage PHP’s existing ecosystem rather than build a new
one. If we were starting from scratch—discarding PHP’s commonly used functions
and learning an entirely new async API—why wouldn’t developers simply switch
languages?
Now that true-async has adopted Swoole’s approach, I think that’s an excellent
choice.
2. Where PHP-FPM falls short
If all you do is read/write MySQL and generate HTML, PHP-FPM is already superb.
If I’m building a web project that only depends on MySQL, I wouldn’t use
Swoole; PHP-FPM is the best choice. But many modern web projects need to call
external HTTP APIs, and slow requests often render PHP-FPM unavailable, which
is frustrating. Async exists precisely to address this. With the rise of
ChatGPT, streaming responses such as SSE and full-duplex communication via
WebSocket will become increasingly common—technologies that PHP-FPM doesn’t
support well. Many developers choose Node.js or Go instead. The influence of
Swoole or amphp remains limited; only a small subset of developers opt to stay
with PHP for async programming using these solutions.
If PHP can adopt true-async or other AsyncIO solutions and provide support for
async I/O at the language level, it would be tremendous news for PHP users. In
essence, async I/O is a runtime matter—much like Node.js in relation to V8. New
PHP syntax isn’t required; Swoole, for instance, adds no new syntax—just some
functions and classes—just as fastcgi_finish_request and fpm_get_status are
php-fpm–only functions.
3. FrankenPHP
FrankenPHP is a wonderful project that uses Go to give PHP additional
capabilities, with great room for exploration.
In an RFC for a Polling API, author Jakub Zelenka—also a FrankenPHP
maintainer—shared a technical idea: consider implementing a goroutine version
of the TSRM thread isolation scheme. Each goroutine would have its own Zend VM
environment—essentially a goroutine-based php-fpm.
I believe this approach may pose significant challenges, especially regarding
memory resources.
Today, when running Symfony or Laravel under PHP-FPM with 100–200 worker
processes, memory pressure is already heavy. If each process consumes tens to
over a hundred megabytes, the group can easily use up to 20 GB. With
goroutines, if you launch thousands or tens of thousands to handle requests
concurrently, memory usage could become enormous.
By contrast, coroutines are designed to be very lightweight: a suspended
coroutine should retain only the call stack and a small amount of
request/session-related memory, while other resources can be shared and reused
across requests. This drastically reduces memory usage while still allowing a
large number of simultaneous requests. When a request is slow, suspension
incurs little cost.
4. Fiber
If Fiber and coroutines coexist as execution units, I agree it can be
confusing. But the current Fiber simply can’t be used in a Swoole-like runtime
with extensive low-level switching.
Although Fiber landed in PHP 8.1, Swoole cannot use any Fiber APIs.
In addition, Fiber doesn’t fully switch all global memory state—for example
OG(handlers), BG(serialize), BG(unserialize)—so it’s unclear whether issues
exist there.
5. Golang’s abundance of synchronization primitives
Go’s goroutine isn’t purely a coroutine; it’s a combination of thread and
coroutine, which necessitates many locks, mutexes, semaphores, and atomics to
resolve data races. PHP does not support multithreading. Whether it’s Fiber,
Swoole, or any other coroutine implementation in PHP, execution is
single-threaded: only one coroutine runs at a time, and until it yields, no
other coroutine runs.
Therefore, PHP coroutines are not a complex concept but a clear and
straightforward one.
If the true-async RFC vote doesn’t pass this time, I think we can split the
work into several parts, aiming for each RFC to accomplish just one thing.
I think the most important task now is to allow coroutine switching in
low-level C code, not just in PHP code. Whether we call it coroutines or Fiber
2.0 is fine. On top of that, other work can be introduced via future RFCs to
progressively strengthen the design.
Lastly, I sincerely hope PHP keeps getting better. Thanks all for your
contributions. Open discussion is always beneficial.
------
Tianfeng Han
------------------ Original ------------------
From: "Edmond Dantes"<[email protected]>;
Date: Fri, Nov 21, 2025 03:17 PM
To: "Rowan Tommins [IMSoP]"<[email protected]>;
Cc: "php internals"<[email protected]>;
Subject: Re: [PHP-DEV] [VOTE] True Async RFC 1.6
Hello.
Imagine that we have an application like this.
```php
class AuthService
{
private static ?self $instance = null;
private PDO $db;
private ?string $sessionId = null;
// Private constructor for singleton
private function __construct(PDO $db)
{
$this->db = $db;
}
// Get singleton instance
public static function getInstance(PDO $db): self
{
if (self::$instance === null) {
self::$instance = new self($db);
}
return self::$instance;
}
public function login(string $email, string $password): bool
{
// Find user by email
$stmt =
$this->db->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
$user =
$stmt->fetch(PDO::FETCH_ASSOC);
// Invalid credentials
if (!$user ||
!password_verify($password, $user['password_hash'])) {
return false;
}
// Generate and save session ID
$this->sessionId =
bin2hex(random_bytes(16));
$stmt = $this->db->prepare(
'INSERT INTO
sessions (user_id, session_id) VALUES (?, ?)'
);
$stmt->execute([$user['id'],
$this->sessionId]);
return true;
}
// Return current session ID
public function getSessionId(): ?string
{
return $this->sessionId;
}
}
```
One day you decide you want more performance and make a single PHP
process handle multiple connections concurrently. You wrap each
request in a separate coroutine and try to use the old code.
```php
$server = new Swoole\Http\Server("127.0.0.1", 9501);
$server->on("request", function ($req, $res) {
// create DB connection (just for example)
$db = new PDO('mysql:host=localhost;dbname=test', 'root',
'');
// get singleton
$auth = AuthService::getInstance($db);
// read request data
$data = json_decode($req->rawContent(), true);
$email = $data['email'] ?? '';
$password = $data['password'] ?? '';
// call old sync code
$ok = $auth->login($email, $password);
if ($ok) {
$res->end("Logged in, session: "
. $auth->getSessionId());
} else {
$res->status(401);
$res->end("Invalid credentials");
}
});
$server->start();
```
What is happening here?
Now, in PHP, inside a single process or thread, the same code is
literally handling multiple connections.
At the same time, there are constant switches between different
requests at the points where MySQL queries occur.
That is, when the code executes
$stmt->execute([$email]);
control is passed to another coroutine with a different
$stmt->execute([$email]);
What breaks in this code?
Correct, coroutines break the singleton because they alternate writing
different Session IDs!
And what does not change in this code?
The SQL queries can remain unchanged.
The first problem with shared memory between coroutines can ONLY be
solved by the programmer. Only the programmer. There is no solution
that would make this happen automatically.
Yesterday we talked about how we can help the programmer detect such
situations during debugging. But in any case, only the programmer
**CAN** and **MUST** solve this problem.
The difference is that you don’t need to rewrite everything else.
The focus is only on the issue of concurrent access to memory.
The essence of the choice is how much code needs to be rewritten.
Almost everything, or only the code with global state.
My choice is: it’s better to rewrite only the code with global state —
or switch to Go and avoid the pain :)
As for the rest, I will write a separate message so as not to clutter things up.
----
Edmond