To correct a mistake in previous email, there was an error in the second code
snippet of my last email. The correct code is as follows:
```php
<?php
Co\run(function() {
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);
Co\go(function() use ($conn) {
while(1) {
fwrite($conn, 'The local time is ' . date('n/j/Y g:i a'));
sleep(1);
}
});
}
});
$n = 2000;
while($n--) {
Co\go(function() {
$fp =
stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
while(1) {
echo fread($fp, 8192),
PHP_EOL;
}
});
}
});
```
The logic here is to start a server coroutine, which then accepts client
connections. Each new connection spawns a coroutine that sends the current time
string to the client every second.
The code below directly creates 2,000 clients in the current process, connects
them to the server, and reads data. Note: If you want to test this code, I
recommend lowering the number of concurrent connections, otherwise your
terminal may freeze or become unresponsive.
This program can accommodate virtually any PHP function, such as mysqli, pdo,
redis, curl, and more—all of which can be executed concurrently with ease.
Notice that only two new APIs have been introduced in the code: `Co\run` and
`Co\go`; the rest are standard or commonly used PHP extension functions.
This is precisely the strength of Swoole, and the focus of the True-Async
team’s ongoing work.
Hopefully, the PHP language will one day include such powerful features
natively.
----------
Tianfeng Han
------------------ Original ------------------
From: "韩天峰"<[email protected]>;
Date: Fri, Nov 21, 2025 07:03 PM
To: "Edmond Dantes"<[email protected]>; "Rowan Tommins
[IMSoP]"<[email protected]>;
Cc: "php internals"<[email protected]>;
Subject: Re: [PHP-DEV] [VOTE] True Async RFC 1.6
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