[PHP-DEV] [VOTE] True Async RFC 1.6

Hi

What I understand from this is that the voters are not willing to make any concessions to include a feature that is important to at least 10% of the developers in the PHP ecosystem (someone mentioned 10% in an earlier email; I would guess the number is even higher, considering China). It’s important for us, who are part of that 10% that apparently isn’t important enough to be heard, to know what the voters’ stance actually is regarding the introduction of tooling for Async in PHP. That way we can understand what the next step needs to be: whether we take on the difficult job of migrating our software from PHP to another platform, or the impossible job of trying to convince people who are not willing to be convinced.

Regards,

Luís Vinícius

Em qui., 20 de nov. de 2025, 12:44, Jakub Zelenka <bukka@php.net> escreveu:

On Thu, Nov 20, 2025 at 1:04 PM Deleu <deleugyn@gmail.com> wrote:

On Thu, 20 Nov 2025 at 07:21 Daniil Gentili <daniil.gentili@gmail.com> wrote:

Hi,

Hello Bart.
I am ready to agree with every word.

Participation from a working group, framework representatives, and the
ability to adapt libraries in advance would remove the concerns that
are currently causing fear. This is probably the only effective
process for developing and introducing such a feature.

Then two days later, you decided that no more discussion was
necessary, and opened a vote.

This feels like a complete contradiction.

Let’s find a way to get that working group set up, and get people from
other projects involved.

My key takeaway from Bart’s message is:

Moreover, even though there are quite a few people in the community
who have the knowledge required because they either develop or work with
aforementioned libraries or extensions, (almost) none of them seem to be
involved in discussing this RFC.
For an RFC that can drastically change the way we develop
applications I would expect more experts on this matter to be involved.
Ideally, PHP core developers, library developers & maintainers, IDE
developers, …, would develop software using this branch to at least
get some feel for the paradigm and this RFC in general.

I absolutely agree with this take, however, so far, the discussion
around this RFC has been, in my opinion, mostly bikeshedding, with
theoretical correctness proposals that are an absolute nightmare in
practice (like structured concurrency), proposed by people that
admittedly have never written extensive amounts of async code in
languages using multiple paradigms, and thus haven’t:

  • Experienced the pain of writing async with colored functions
  • Experienced the footguns of structured concurrency
  • And on the other hand, haven’t experienced the pleasure and simplicity
    of safely writing async code in languages like Go, or in PHP using AMPHP
    (which use uncolored and unstructured concurrency, the kind proposed and
    championed by edmond)

While a working group can steer the conversation away from
theoretically correct but practically unusable approaches, that can
happen only if

  • The correct people (i.e. async library maintainers, or people that
    write async logic every day in multiple languages like myself) are present
  • They are given more weight than the average PHP developer who hasn’t
    used async much if at all, and can only make theoretical proposals not
    based on practice and experience

I’m afraid that given the current state of the PHP community, which is
largely new to async, the quality of the conversation in a working group
would not be much higher than the one I’m seeing in this RFC, and would
just protract even longer the agony of design by committee, where in
reality what’s needed is a single, clear and correct vision (which
Edmond has), without influences from unexperienced people making
proposals based purely on abstract/theoretical PoVs.

Regards,

Daniil Gentili.

While I certainly can sympathize with the painful, dreadful, unpleasant, unbearable agony of debating a subject with “non-experts”, it’s important to have some perspective in the opposite direction.

As it has been mentioned before, Async PHP in general is practically a rounding error in terms of user base and there are reasons for that. It’s important to remember that the benefits of async doesn’t always justify the burden that it brings. For PHP as a language to adopt an async solution natively it’s very important that sync code continues to function while also allowing developers to opt into async without having to feel like they changed languages and must re-learn how to manage their projects. If this is not possible then perhaps the current state is as good as we can ever get: let expert matter install their extension (opt-in) on a per-project basis.

It’s going to be up to the subject experts to come up with a path that allows PHP to stay coherent while offering both approaches. To put this in another way: RFC Voters are above average PHP developers. If they’re unable to digest the changes being proposed, even if said changes are being proposed by the single most subject-expert human on the planet, then how do we expect average PHP developers to make good use of it?

Yeah I think this is one of the reasons why the RFC failed. It couldn’t properly explain the topic even though it was reduced to minimum. One of the factor is certainly that Edmond is new to the RFC process but more importantly it’s quite contentious topic that can bring even more bike shedding. I think there were some important points raised in the discussions about safety of the existing sync code which I think might be the real killer here. So even if we omit the mix up with the pre-announcement and sudden voting (that were sure path to rejection), I think the bigger problem is the whole size of the feature and the fact that it will be extremely hard to find any solution that will please majority of voters. I’m honestly not sure if this is possible to get to any form that can pass. I would like to be wrong but we can see the reality here.

I think the way forward for PHP is to do what we have been doing and it is to provide the building blocks for user space to enable async there because that’s something that can be reasonably introduced using the RFC through smaller chunks. It means improving the non blocking setup, exposing IO hooks, better polling and other primitives. That was actually the plan in past and that’s why it is also contained in my STF stream work where the scope was created way before the TrueAsync.

Kind regards,

Jakub

1 Like

Enviado com um e-mail seguro do Proton Mail.

Em quarta-feira, 19 de novembro de 2025 às 09:38, Edmond Dantes <edmond.ht@gmail.com> escreveu:

Hello all

According to all previous discussions, version 1.6 of this RFC has
been prepared and is now being submitted for a vote:

Voting Page: PHP: rfc:true_async:voting
RFC PHP: rfc:true_async

The vote officially starts tomorrow, as previously announced.

For version 1.6 the following important change was made:
All input/output functions are now bound by the shared requirement of
being non-blocking with respect to the process. However, the specific
behavior of each function may (optionally) be defined in separate
RFCs.

Thus, I/O functions themselves are not part of this RFC, but the main
RFC defines the general way in which they must operate. Thus (as I see
it), the RFC achieves a balance between cohesion and separation of
concerns.

Since the discussion period has ended, I will not be engaging in
further debate (except regarding the voting process itself). If you
have any questions for me of any kind, you may ask them either in a
separate thread or privately. (This also means that I will not be
answering RFC-related questions in this thread). I will be glad to
hear your opinions and feedback. I wish all participants the best of
luck.

---
Best Regards, Ed

Most of the people who advocate for this are responsible for the language's backwardness; they don't know how to conduct a conversation, it's just their opinion and nothing more. Worse, many times they aren't even using the language; they're stuck in the past, like dinosaurs. And the conversation is always the same: "Want asynchronous programming? Change languages, go to Node, switch to Go," but it's not that simple.

I'm amazed at how shallow their knowledge is, yet they defend the cause as if they were experts, without even trying to delve deeper into the subject. Those who do this don't lift a finger to move in that direction, and instead they come up with things nobody asked for, insignificant things that only 1% of people will use.

They say few people use asynchronous PHP. The adoption is totally different when the language already offers it natively. "Ah, but it will only be used by a small percentage of PHP users," and what about the useless things we didn't ask for that will only be used by 1%?

You could argue that it's easy to say, that they dedicate time from their hobby to improving PHP, and I appreciate that, but someone, in this case Ed, also dedicated time to it.

On 20/11/2025 18:42, carlos_silvaaaaa wrote:

Most of the people who ...

I'm going to stop you there.

Personal attacks are not acceptable.

Making assumptions about what other people think is not helpful.

Framing the discussion as "us vs them" is not productive.

Please, anyone thinking of replying with vague opinions and rants, stop. We're here to make the language better, not to trade insults.

Thank you.

--
Rowan Tommins
[IMSoP]

Hello all.

I’d like to add a few words regarding the 10 percent. Let’s assume
there really are 10% of developers in PHP who use Async. We can
replace this with some number X. What’s important is how exactly these
percentages are calculated.

For example, if this X percent is calculated based on the total number
of projects, that’s one thing. Suppose PHP is used in 1,000 projects.
Out of those, 800 projects are fairly simple, 150 are medium-sized,
and 50 are complex.

Most likely, async would be used in 200 projects, not in all 800. So
if there truly are X% of developers using it, then it turns out this
is actually a very large percentage of developers working on medium
and complex projects.
Do you see the difference?

The difference is that medium and complex projects bring PHP
developers more money. This means that if the PHP language and its
ecosystem cannot compete with Go or Python, then PHP developers earn
less because they’re not participating in the projects that generate
that money. Even if only 5% of developers use synchronous PHP, that
can still be a lot when you recalculate it.

And it is precisely the segment of developers working on medium- and
high-complexity tasks that pushes the language forward.
Therefore, losing 10%, or 5%, or even 2% may actually cost much more
for the language’s progress than it seems.

Skype and Nokia were once great too, and they also decided not to
develop their “1%.”

On 20/11/2025 10:18, Daniil Gentili wrote:

I absolutely agree with this take, however, so far, the discussion around this RFC has been, in my opinion, mostly bikeshedding, with theoretical correctness proposals that are an absolute nightmare in practice (like structured concurrency), proposed by people that admittedly have never written extensive amounts of async code in languages using multiple paradigms

That is exactly why I think some other process is needed, and why an RFC vote right now doesn't tell us anything useful.

I really want this feature. But I really want us to get it right.

As someone who is completely new to the topic, the things that will make me vote "Yes" on an async proposal, are:

1) Confidence that experts have reviewed the design, and ironed out crucial details.

From what I've seen, Edmond has done an extremely thorough job. Once again, thank you, I tip my hat to your effort and dedication. But I'm not qualified to judge the result, and I want to see opinions from people who are.

It may be that after discussing it with a bunch of other experts, exactly the same technical design would come out; I just want a bit of reassurance that the right discussions have happened.

Has there been an attempt to invite specific people into a discussion, e.g. people who've worked on ReactPHP, Swoole, etc?

2) A cast-iron promise that existing code would run unchanged; or a clear explanation of what modifications it would need.

The "Goals" section says code will not need changes "or changes should be minimal"; and will run without modification "provided that..." Those caveats worry me.

I work on a code base with a million lines of PHP code developed in half a dozen frameworks over twenty years, maintained by a small team. When I hear "it took us 2-3 days to update a library", I don't hear a small number; I multiply it in my head, and wonder how I would justify weeks of development and testing. Right now, I don't even understand what we would be spending those weeks doing.

If it really is a case of "it will be fine unless you're doing some weird tricks to force PHP to do things it doesn't normally do", then great!

If it ends up as "set this global setting, and you won't get the benefit, but your legacy code will run fine", I can live with that.

If the only advice is "your code will be fine as long it's already well-architected and completely covered by automated testing", it's no use to me - that's just not the reality I live in.

I sincerely hope that this is solvable.

3) A set of "idiot-proof" high-level features that a non-expert PHP developer can use without understanding the full implementation.

The current RFC is a lot more focused than previous versions, and that is great; but it still gets very quickly into the mechanics and edge-cases that require expertise to use.

What I think is missing is the pitch to users. Something that we can put on the php.net homepage, and say "look how easy concurrency is in PHP 9!"

To paraphrase the Perl slogan, I want the language to make the common things easy, and the complex things possible. I want a "pit of success", where the easiest thing to write is also the most likely to be safe and useful.

I think that mostly just means defining some syntax sugar on top of some of the functions and objects. Something that we can say is PHP's equivalent of async/await, or goroutines and channels.

But ... I realise this can't all fit into one RFC without discussion going on forever.

Which is why we need to find some new process to make it possible.

On 20/11/2025 15:39, Jakub Zelenka wrote:

I think the way forward for PHP is to do what we have been doing and it is to provide the building blocks for user space to enable async there because that's something that can be reasonably introduced using the RFC through smaller chunks.

I think it would be a shame to throw away the progress that's been made.

I wonder if a way forward could be something like this:

1) Agree a Project Charter, some Goals and Non-Goals. Hold a vote on this, to agree that we're going ahead with the project

2) Set up some kind of project tracker, where we can list open questions and design tasks; maybe a separate mailing list / forum / chatroom for those involved in the details

3) Start with Edmond's amazing work, and iteratively work on those individual questions

4) Keep bikeshedding questions (e.g. "what is the best name for this class, and should it extend Error or Exception?") separate from architecture questions (e.g. "is this class necessary, or should it be hidden from the user?")

5) Converge on a solution where we've already agreed everything in bite-sized pieces

6) Release PHP 9.0 and celebrate

All of that requires that enough people who actually understand the problem space deeply are willing to be involved and collaborate. I really hope that's the case, but I am not in a position to volunteer myself.

--
Rowan Tommins
[IMSoP]

Hello.

Imagine that we have an application like this.

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.

$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 :slight_smile:

As for the rest, I will write a separate message so as not to clutter things up.

----
Edmond

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.

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

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

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

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

1 Like

Hi,

On Fri, Nov 21, 2025 at 8:16 AM Edmond Dantes <edmond.ht@gmail.com> wrote:

Hello.

Imagine that we have an application like this.

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.

$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!

I think you seriously underestimate impact of this in the current PHP code bases where many applications depend on global state. Especially the legacy ones but even the most popular ones. Just look into WordPress which use global state extensively. Now imagine that some popular plugin decides to use async which change some of its globals (like $post or $wp_query) during the suspension of the main code. I would assume this could horribly break things. Don’t forget that other code don’t have control over the plugin and it might not even know that async is used there. So I’m not sure if this design is compatible with WordPress and similar applications where global state is used extensively. If that’s the case, it’s of course a problem because those applications (well WordPress on its own in fact) compose the majority of PHP users so introducing something that would have potential to break its code would limit usability of the whole feature and could even lead to loosing more users that we could gain from introducing this feature.

So I think it will need to find some solution that will prevent this from happening. I guess there might be few options

  1. Disallow suspension of the main sync code which is effectively some sort of colouring.
  2. Preventing access to globals from coroutine which I’m not sure is even fully doable from the engine PoV - it would mean some sort of different execution mode that could not use globals (e.g. global keyword and calling some functions that change global state). It would need channels for communications between coroutines. The advantage of such model would be possibility to combine it with threads in the future but I could imagine it could still lead to some subtle issue for years as there is internal global state as well that can lead to some surprises. But maybe that would be worth it.

Just some thoughts…

Cheers

Jakub

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
Co\run(function() {
&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $conn = stream_socket_accept($fp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Co\go(function() use ($conn) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwrite($conn, 'The local time is ' . date('n/j/Y g:i a'));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });

&nbsp; &nbsp; $n = 2000;
&nbsp; &nbsp; while($n--) {
&nbsp; &nbsp; &nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo fread($fp, 8192), PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; }
});

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

&nbsp;
------------------&nbsp;Original&nbsp;------------------
From: &nbsp;"韩天峰"<rango@swoole.com&gt;;
Date: &nbsp;Fri, Nov 21, 2025 07:03 PM
To: &nbsp;"Edmond Dantes"<edmond.ht@gmail.com&gt;; "Rowan Tommins [IMSoP]"<imsop.php@rwec.co.uk&gt;;
Cc: &nbsp;"php internals"<internals@lists.php.net&gt;;
Subject: &nbsp;Re: [PHP-DEV] [VOTE] True Async RFC 1.6

&nbsp;

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() {
&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo fread($fp, 8192), PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });

&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $conn = stream_socket_accept($fp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwrite($conn, 'The local time is ' . date('n/j/Y g:i a'));
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });

&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $redis = new Redis();
&nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;connect('127.0.0.1', 6379);
&nbsp; &nbsp; &nbsp; &nbsp; while(true) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;subscribe(['test'], function ($instance, $channelName, $message) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo 'New redis message: '.$channelName, "==&gt;", $message, PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });

&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $redis = new Redis();
&nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;connect('127.0.0.1', 6379);
&nbsp; &nbsp; &nbsp; &nbsp; $count = 0;
&nbsp; &nbsp; &nbsp; &nbsp; while(true) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(2);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;publish('test','hello, world, count='.$count++);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });
});

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() {
&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo fread($fp, 8192), PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });

&nbsp; &nbsp; $n = 2000;
&nbsp; &nbsp; while($n--) {
&nbsp; &nbsp; &nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $conn = stream_socket_accept($fp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwrite($conn, 'The local time is ' . date('n/j/Y g:i a'));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; }
});

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

&nbsp;

------------------ Original ------------------
From: &nbsp;"Edmond Dantes"<edmond.ht@gmail.com&gt;;
Date: &nbsp;Fri, Nov 21, 2025 03:17 PM
To: &nbsp;"Rowan Tommins [IMSoP]"<imsop.php@rwec.co.uk&gt;;
Cc: &nbsp;"php internals"<internals@lists.php.net&gt;;
Subject: &nbsp;Re: [PHP-DEV] [VOTE] True Async RFC 1.6

&nbsp;

Hello.

Imagine that we have an application like this.

class AuthService
{
&nbsp; &nbsp; private static ?self $instance = null;

&nbsp; &nbsp; private PDO $db;
&nbsp; &nbsp; private ?string $sessionId = null;

&nbsp; &nbsp; // Private constructor for singleton
&nbsp; &nbsp; private function __construct(PDO $db)
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $this-&gt;db = $db;
&nbsp; &nbsp; }

&nbsp; &nbsp; // Get singleton instance
&nbsp; &nbsp; public static function getInstance(PDO $db): self
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; if (self::$instance === null) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; self::$instance = new self($db);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return self::$instance;
&nbsp; &nbsp; }

&nbsp; &nbsp; public function login(string $email, string $password): bool
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; // Find user by email
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt = $this-&gt;db-&gt;prepare('SELECT * FROM users WHERE email = ?');
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt-&gt;execute([$email]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $user = $stmt-&gt;fetch(PDO::FETCH_ASSOC);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; // Invalid credentials
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; if (!$user || !password_verify($password, $user['password_hash'])) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return false;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; // Generate and save session ID
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $this-&gt;sessionId = bin2hex(random_bytes(16));

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt = $this-&gt;db-&gt;prepare(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; 'INSERT INTO sessions (user_id, session_id) VALUES (?, ?)'
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; );
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt-&gt;execute([$user['id'], $this-&gt;sessionId]);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return true;
&nbsp; &nbsp; }

&nbsp; &nbsp; // Return current session ID
&nbsp; &nbsp; public function getSessionId(): ?string
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return $this-&gt;sessionId;
&nbsp; &nbsp; }
}

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.

$server = new Swoole\Http\Server("127.0.0.1", 9501);

$server-&gt;on("request", function ($req, $res) {

&nbsp; &nbsp; // create DB connection (just for example)
&nbsp; &nbsp; $db = new PDO('mysql:host=localhost;dbname=test', 'root', '');

&nbsp; &nbsp; // get singleton
&nbsp; &nbsp; $auth = AuthService::getInstance($db);

&nbsp; &nbsp; // read request data
&nbsp; &nbsp; $data = json_decode($req-&gt;rawContent(), true);

&nbsp; &nbsp; $email = $data['email'] ?? '';
&nbsp; &nbsp; $password = $data['password'] ?? '';

&nbsp; &nbsp; // call old sync code
&nbsp; &nbsp; $ok = $auth-&gt;login($email, $password);

&nbsp; &nbsp; if ($ok) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $res-&gt;end("Logged in, session: " . $auth-&gt;getSessionId());
&nbsp; &nbsp; } else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $res-&gt;status(401);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $res-&gt;end("Invalid credentials");
&nbsp; &nbsp; }
});

$server-&gt;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-&gt;execute([$email]);
control is passed to another coroutine with a different
$stmt-&gt;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 :slight_smile:

As for the rest, I will write a separate message so as not to clutter things up.

----
Edmond

Hello,

Thank you a lot.

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?

And it turned out to be a very successful solution, which has been
proven many times in practice.
The first question developers ask when they want to migrate to async
is: how much code do I need to rewrite?
If the cost of migration is comparable to rewriting the entire project
from scratch, then it becomes economically more sensible to hire Go
developers and rebuild the project.

And PHP’s influence is strong primarily because of the companies that
run PHP-based projects.
So when a company decides to rewrite a project in another language
because PHP no longer meets its technical needs, PHP loses that
client.
That’s why supporting a single runtime and a single API,
standardization, is crucial for the technology’s survival.

Those were the economic reasons. There are technical reasons as well.
In a server that handles multiple requests within a single thread, it
is very important that coroutines do not occupy the CPU for too long.
If they do, all coroutines in that process are affected.
This means that having blocking operations inside a coroutine
nullifies the advantages of concurrency.
Therefore, intentionally making functions blocking so that code works
correctly with shared memory makes the idea of using asynchronous code
**pointless**.

This does not mean that one solution or another is 100 percent
perfect. Each has its drawbacks. The question is whether the drawbacks
outweigh the benefits.
Swoole made it possible to see that, in practice, using a unified API
provides a significant advantage that makes migrating legacy projects
feasible.
This means that by voting for an RFC built on these principles, we are
not venturing into a dark unknown, but walking along a well-trodden
path.

----
Ed

Hello

I think you seriously underestimate impact of this in the current PHP code bases where many applications depend on global state.

Do I really look like someone who could underestimate memory-related issues? :slight_smile:

Now imagine that some popular plugin decides to use async which change some of its globals (like $post or $wp_query) during the suspension of the main code. I would assume this could horribly break things.

Or for example, you run an old WordPress on the new PHP 8.5 and
everything breaks. Is that a reason not to release new PHP versions?
No, I’m not joking. That’s literally the essence of the argument. If
something might break, then we shouldn’t do it?

This is a common story. There is a framework that is adapted to a
technology, and there is a framework or library that is not adapted to
it.
If a framework is not adapted, you simply won’t be able to use the
technology. So why is this considered a problem?
However, inside WordPress you will still be able to use coroutines as
long as you don’t call WP functions that aren’t adapted. You can. So
why is this a problem?

I can use AMPHP inside WordPress and break WP.
So does that mean we must urgently remove Fiber from the language?
Because AMPHP uses Fiber, and AMPHP implements coroutines. And
coroutines break WordPress.
Asynchrony already exists in PHP. You can write async code today.
Which means you can already do all the “horrors” you’re talking about.
TrueAsync cannot change that. And no one else can change it either.
So why is this being treated as an argument against it?

Don't forget that other code don't have control over the plugin and it might not even know that async is used there.

Exactly. This means that right now I can use Fiber plus select() to
write async code and break WordPress.
And I can also write a plugin that divides by zero and crashes
WordPress, and WordPress won’t know anything about it.

So I'm not sure if this design is compatible with WordPress

Yes, WordPress is not compatible with asynchrony. I’ll emphasize
again. This is not about TrueAsync specifically. This is about
asynchrony itself. Yes, WordPress and Laravel are not compatible with
concurrent execution. That’s true.
But are you **really suggesting** that because of this, all other
applications should be denied the ability to use it?
Moreover, would you deny WordPress itself the possibility of
supporting asynchrony in future? After all, WordPress can be
refactored. It’s not carved in stone.

Wouldn’t WordPress benefit from the performance improvements that
async provides?
Wouldn’t WordPress plugins benefit from being able to actively
communicate with microservices and deliver the fastest possible
responses to JavaScript?

Is this feature really something nobody needs? If yes, then I have no
further questions.

Async is needed to increase throughput. That’s the purpose. If a PHP
project doesn’t need it, it doesn’t have to use it. That’s fine. But
there are PHP projects that do need it.

Good morning from a rather cold but sunny Belgium,

About Backwards compatibility & colored functions.

Right now, I think there's a problem with the promise of backward
compatibility. Unless your application handles shared state perfectly,
switching on async PHP is going to cause a lot of problems. Ignoring
the problem and asking users to rewrite shared state in their
applications is an absolute no-go from me. On the other hand, we could
go down the path of colored functions, which is also an absolute no-go
from me. We've had this in the past before Fibers were a thing and it
was an absolute pain in the ass. Every function only accepted
Promise<x> and only returned Promise<x>. Bear in mind this coincided
with the rise of static typing and FIG-PSRs where you could get away
with it until Interfaces accepted and returned actual types. Having
colored functions could mean having to duplicate all interfaces where
you would have sync and async ones. This would split the ecosystem in
two and kill adoption, no thanks.

I would much rather have some solution where we can enable (opt-in)
non-blocking/async behavior on a per-project basis. At the very least
we'll be able to use it in greenfields projects. Developers who want
to add support for non-blocking/async I/O in their existing
applications can then rewrite their global state and turn it on
whenever they feel ready for it. It's not ideal but I think it's a
nice compromise between fast adoption and backward compatibility. I
envision a time where you'd look at the documentation of a library and
see "ok, this is async-ready" (shared state is dealt with) through
some badge or something.

---

For people who are questioning whether a feature like this should be
added to PHP I'd like to point out a couple of things.

1. Current usage of async PHP is not a good argument. Like I've
previously said elsewhere, this is a classical chicken-or-egg thing.
Some people think it's not worth adding support for this because
nobody is using it but also very few people are using async PHP
because it's not a first class citizen in the language. Having a
couple of libraries and frameworks supporting whatever async model PHP
ends up choosing would increase these numbers drastically. Adding
support for this new paradigm in popular frameworks/CMSes and thereby
pushing the language and ecosystem forward seems like something the
PHP Foundation was founded for (wrong mailing list, I know).

2. About handing off the problem to Userland: you can _kinda_ do
things asynchronously in PHP through Userland (I'm talking Fibers)
these days, but having async as a first class citizen in PHP core
would make things much easier. Currently, the ecosystem is fragmented
and you need a lot of libraries and specific knowledge in order to get
started. We've been doing this for a couple of years now and only
recently we got a decent ORM to work async (one unit of work per
Fiber, transactions, ...).

3. I think PHP is especially suited for this paradigm. Web
applications require a lot of I/O, whether it is talking to the
client, database, filesystem, API's, ... Having the ability to either
serve multiple requests at the same time through a long running
process or - for example - upload a file to S3 and concurrently write
a record registering that file in the database can lower the TTFB of
web applications drastically. Moreover, as shared hosting is a thing,
spawning more processes or threads isn't always going to be available.

I feel like not adding async to PHP in the (near?) future would only
be postponing it and risk developers looking to squeeze that extra bit
of performance from their applications to _Go_ to other languages.

---

On a final note, it's nice to see this discussion is gaining some
traction! I've just checked with core ReactPHP developers and they're
checking up on the RFC, however, their main concern seems to be the
same as others on this list: time. Everything is changing very fast
and more time is needed to digest this all and to get used to the idea
of this mentally.

I agree with others, this is something that we have to do right, so
let's take the time to do it right. There's already been an awesome
effort by Edmund, let's keep working on this.

Best Regards,
Bart Vanhoutte

Op vr 21 nov 2025 om 13:08 schreef Edmond Dantes <edmond.ht@gmail.com>:

Hello

> I think you seriously underestimate impact of this in the current PHP code bases where many applications depend on global state.
Do I really look like someone who could underestimate memory-related issues? :slight_smile:

> Now imagine that some popular plugin decides to use async which change some of its globals (like $post or $wp_query) during the suspension of the main code. I would assume this could horribly break things.
Or for example, you run an old WordPress on the new PHP 8.5 and
everything breaks. Is that a reason not to release new PHP versions?
No, I’m not joking. That’s literally the essence of the argument. If
something might break, then we shouldn’t do it?

This is a common story. There is a framework that is adapted to a
technology, and there is a framework or library that is not adapted to
it.
If a framework is not adapted, you simply won’t be able to use the
technology. So why is this considered a problem?
However, inside WordPress you will still be able to use coroutines as
long as you don’t call WP functions that aren’t adapted. You can. So
why is this a problem?

I can use AMPHP inside WordPress and break WP.
So does that mean we must urgently remove Fiber from the language?
Because AMPHP uses Fiber, and AMPHP implements coroutines. And
coroutines break WordPress.
Asynchrony already exists in PHP. You can write async code today.
Which means you can already do all the “horrors” you’re talking about.
TrueAsync cannot change that. And no one else can change it either.
So why is this being treated as an argument against it?

> Don't forget that other code don't have control over the plugin and it might not even know that async is used there.
Exactly. This means that right now I can use Fiber plus select() to
write async code and break WordPress.
And I can also write a plugin that divides by zero and crashes
WordPress, and WordPress won’t know anything about it.

> So I'm not sure if this design is compatible with WordPress

Yes, WordPress is not compatible with asynchrony. I’ll emphasize
again. This is not about TrueAsync specifically. This is about
asynchrony itself. Yes, WordPress and Laravel are not compatible with
concurrent execution. That’s true.
But are you **really suggesting** that because of this, all other
applications should be denied the ability to use it?
Moreover, would you deny WordPress itself the possibility of
supporting asynchrony in future? After all, WordPress can be
refactored. It’s not carved in stone.

Wouldn’t WordPress benefit from the performance improvements that
async provides?
Wouldn’t WordPress plugins benefit from being able to actively
communicate with microservices and deliver the fastest possible
responses to JavaScript?

Is this feature really something nobody needs? If yes, then I have no
further questions.

Async is needed to increase throughput. That’s the purpose. If a PHP
project doesn’t need it, it doesn’t have to use it. That’s fine. But
there are PHP projects that do need it.

As for next lines:

1. Disallow suspension of the main sync code which is effectively some sort of colouring.
2. Preventing access to globals from coroutine which I'm not sure is even fully doable from the engine PoV

Yesterday there was a small discussion about debugging capabilities in
PHP for async.
For example: throwing an error if more than one coroutine writes to a variable.
Or issuing a warning if someone creates a coroutine inside a function.

This can be called an extended debugging mode. Almost no other
language has this, and if we implement it, it would be very cool and
very useful.

We can also come up with other ways to protect PHP users from silly mistakes.
For example, blocking async entirely or per namespace.
But I don’t think discussing these features is the top priority right now.
All of this is nice to have once everything else is already in place.

Hello Bart.

Good morning from a rather cold but sunny Belgium,

Cool, it’s +12° here today and sunny as well :slight_smile:

I would much rather have some solution where we can enable (opt-in) non-blocking/async behavior on a per-project basis.

Agreed, this will make it possible to disable asynchrony in projects
that are not ready for it yet.
And we can come up with several ways to do that.

---
Ed

On Fri, 21 Nov 2025 at 09:08 Edmond Dantes <edmond.ht@gmail.com> wrote:

Hello

I think you seriously underestimate impact of this in the current PHP code bases where many applications depend on global state.
Do I really look like someone who could underestimate memory-related issues? :slight_smile:

Now imagine that some popular plugin decides to use async which change some of its globals (like $post or $wp_query) during the suspension of the main code. I would assume this could horribly break things.
Or for example, you run an old WordPress on the new PHP 8.5 and
everything breaks. Is that a reason not to release new PHP versions?
No, I’m not joking. That’s literally the essence of the argument. If
something might break, then we shouldn’t do it?

This is a common story. There is a framework that is adapted to a
technology, and there is a framework or library that is not adapted to
it.
If a framework is not adapted, you simply won’t be able to use the
technology. So why is this considered a problem?
However, inside WordPress you will still be able to use coroutines as
long as you don’t call WP functions that aren’t adapted. You can. So
why is this a problem?

I can use AMPHP inside WordPress and break WP.
So does that mean we must urgently remove Fiber from the language?
Because AMPHP uses Fiber, and AMPHP implements coroutines. And
coroutines break WordPress.
Asynchrony already exists in PHP. You can write async code today.
Which means you can already do all the “horrors” you’re talking about.
TrueAsync cannot change that. And no one else can change it either.
So why is this being treated as an argument against it?

Don’t forget that other code don’t have control over the plugin and it might not even know that async is used there.
Exactly. This means that right now I can use Fiber plus select() to
write async code and break WordPress.
And I can also write a plugin that divides by zero and crashes
WordPress, and WordPress won’t know anything about it.

So I’m not sure if this design is compatible with WordPress

Yes, WordPress is not compatible with asynchrony. I’ll emphasize
again. This is not about TrueAsync specifically. This is about
asynchrony itself. Yes, WordPress and Laravel are not compatible with
concurrent execution. That’s true.
But are you really suggesting that because of this, all other
applications should be denied the ability to use it?
Moreover, would you deny WordPress itself the possibility of
supporting asynchrony in future? After all, WordPress can be
refactored. It’s not carved in stone.

Wouldn’t WordPress benefit from the performance improvements that
async provides?
Wouldn’t WordPress plugins benefit from being able to actively
communicate with microservices and deliver the fastest possible
responses to JavaScript?

Is this feature really something nobody needs? If yes, then I have no
further questions.

Async is needed to increase throughput. That’s the purpose. If a PHP
project doesn’t need it, it doesn’t have to use it. That’s fine. But
there are PHP projects that do need it.

The point you’re trying to make here is very clouded for me. Although you can use AMPHP to break Wordpress, there is an extensive process of “opt-in” for you to do that and it doesn’t happen naturally, accidentally or unintentionally. You’ll have to setup a plugin that requires AMPHP and write some code that will inadvertently break and never really see the light of day.

Now there’s 2 possibilities for us to go from here: my understanding of the most basic concept of TrueAsync is still wrong OR the comparison you’re trying to make isn’t relevant.

My understanding is that if this RFC were to be implemented as-is, you would be able to open any PHP file and write \Async\spawn() and: nothing would blow up but any code inside the project that touches global state could silently misbehave. Is this not true?

My understanding of AMPHP is that things only become async if you explicitly run AMPHP as your web server. It either has no effect if your application is running Apache + mod_php OR your application 100% breaks with nothing getting executed at all. Is this not true?

I know you’re tired of debating very basic things, but from the conversation on the list, I think I’m not the only one confused by this. We did talk about our desire to have Async on PHP-FPM and as far as I remember you mentioned it not being very useful, but that it would be possible nonetheless. This is the a core principle that matters a lot.

Here is a very simple hypothetical:
Suppose I run a website and I have very little understanding of PHP, but I know the basics. Every Monday I run composer update, do a little testing and deploy the website. Suppose next Monday when I run composer update one of my dependencies (Package A) starts to pull in AMPHP as its nested dependency. When I try to open my website locally it will either have everything broken or it will just work regularly and nothing will be running async. There is no other option.
Now suppose the same example but with PHP 9 + TrueAsync. I do composer update one day and test my home page and it works. But deeply nested somewhere there is a feature that will use Package A which then spawns a coroutine and leads to my global state code behaving weirdly. It’s not a fatal error. It’s not very clear at all to me that something changed, but now my array indexes are triggering DB updates out of order.

If my assumptions about TrueAsync are completely wrong and that’s not possible at all, I think that’s not clear for everyone. But if my assumptions are right, then your comparison with AMPHP is not the same thing.

Hello.

To use AMPHP you don’t need to run its server. But let’s simplify the situation.

You have Fiber.
With Fiber you can build an EventLoop + Scheduler, create two
coroutines, and then run code inside them that, for example, renders a
template.

It will require a fair amount of code, but it’s possible.
At the same time, ob_start/ob_end will immediately break, because they
don’t support Fiber switching. This is a known bug.
And if you pass global state into closures, and that state changes
unpredictably, the application logic will essentially break.

**What’s different with TrueAsync**

1. You don’t need to write a lot of code. Yes, just spawn();
2. `ob_start` and `ob_end` will work correctly.
3. If closures share a variable that multiple coroutines write to
unpredictably, the application will still break.

In other words, no matter how asynchrony is implemented in PHP (and
other langs), it will always require correct handling of shared
memory.
And I want to clarify a bit. This is not about a global variable. It’s
about shared memory. These are not the same things. A local variable
can be passed into a closure by reference.
But if you don’t call WP() inside a coroutine, nothing prevents you
from using asynchrony.

In TrueAsync there is a Dockerfile for FPM, you can install it locally
and try running WordPress. I’m sure something will break, but not
everything at once :slight_smile:

My understanding of AMPHP is that things only become async if you explicitly run AMPHP as your web server. It either has no effect
if your application is running Apache + mod_php OR your application 100% breaks with nothing getting executed at all. Is this not true?

You can use it without a server. The only difference is that the
coroutines won’t live longer than the HTTP request. The same applies
to TrueAsync when running under FPM.

When I try to open my website locally it will either have everything broken or it will just work regularly and nothing will be running async. There is no other option.

It won’t break on its own.
It will only break if that “something” shares a variable or resource
with your code. If you write code according to SOLID principles, the
chance of breakage is very low.

If someone writes coroutine code and completely ignores memory
handling, it’s the same as writing an infinite loop. Can they do that?
Yes. Can you download that code? Yes. Can it break everything? It can.
What’s the conclusion?

I do `composer update` one day and test my home page and it works. But deeply nested somewhere there is a feature that will use Package A which
then spawns a coroutine and leads to my global state code behaving weirdly. It's not a fatal error. It's not very clear at all to me that something changed,
but now my array indexes are triggering DB updates out of order.

The exact same thing will happen in code without coroutines if, for
example, you have a class, you pass an array to it by reference,
and that class silently corrupts your values. You don’t need
asynchrony for that to happen.

<?php

class Storage
{
    private array &$data; // holds a reference to the external array

    public function __construct(array &$data)
    {
        // store the reference
        $this->data = &$data;
    }

    public function loadUser()
    {
        // looks like a harmless read
        return $this->data['user'] ?? null;
    }

    public function updateInternally()
    {
        // silently mutates the external array
        $this->data['user'] = 'hacked';
        $this->data['counter'] = ($this->data['counter'] ?? 0) + 1;
    }
}

// ---------------------

$shared = [
    'user' => 'admin',
    'counter' => 0,
];

// create the class, passing the array by reference
$storage = new Storage($shared);

echo $storage->loadUser() . PHP_EOL; // admin

// somewhere else this method is called, modifying the shared array
$storage->updateInternally();

print_r($shared);
// the array is now corrupted unexpectedly

There is no asynchrony in the example above. It’s just normal code. It
breaks application logic because the class secretly holds the array
and modifies it without permission.

And a coroutine is just a regular object. If you pass a referenced
array into a coroutine and then say something “accidentally” broke, it
wasn’t accidental. Did the array get there by accident? No. Did some
developer write the code by accident? No. It’s a bit different from
synchronous code, but the meaning is exactly the same.
I have a mental habit: when I write asynchronous code, I always assume
that any object shared between coroutines can change at any moment.
Similarly, if you hand off memory ownership to another class or share
memory, you have to keep in mind that there is a chance some developer
might accidentally do something wrong.

The only difference between synchronous and asynchronous code is that
you don’t have a precise moment in time when the change happens. And
yes, that makes debugging harder. It makes finding bugs harder. But
it’s not something fundamentally new.

Yes, making the same mistakes with AMPHP is harder, because you need
to install a whole package and so on. But that’s not a guarantee :slight_smile:

I don’t know how else to describe it, but I decided to give a last attempt here since you mentioned your mental habit: having a mental habit about async code makes perfect sense. What I’m trying to say is that most PHP developers don’t have that mental habit and that’s not a problem because Async code in PHP doesn’t come into your project out of nowhere. With spawn() being native, anytime you do “composer update” your project may start running async code without your consent and without warning you that you should go through a mental habit that you don’t even know exist because you never had to worry about async code before in your life.

Having spawn() be an extremely easy way to start running async code is a great feature (not a bug). But the very first thing that seems to be missing is: I’m a dumb PHP developer that don’t know and don’t care about async php and when I upgrade to PHP X.Y, how do I keep my project always-sync without risking one of my composer packages suddenly calling spawn() and causing bugs I have no idea how to even begin to understand?

ini settings are very frowned upon, but for the sake of a conversation, we can think of it like that. if I do async.enabled = 0, then nothing changes for me. Having this opt-in/opt-out control would mean that a broken website caused by spawn() only happens if they decide to enable async and they can start learning and developing their mental habit about how to work with async code.

···

Marco Deleu

Hello

how do I keep my project always-sync without risking one of my composer packages suddenly calling spawn() and causing bugs I have no idea how to even begin to understand?

This idea was mentioned a bit earlier in the discussion. You can use a
special setting to disable asynchrony. There are many different
possibilities here. For example, you can tell Composer not to use
async packages. And so on. You can come up with your own protection
mechanisms. There is no technical problem here in terms of
implementation.
Why not enable asynchronous mode in PHP explicitly through php.ini?

I would follow this logic:

* Since this is a new feature, it should be disabled by default.
* Therefore, to use it, you must explicitly enable it.

For example, you can tell Composer not to use async packages.

This is a good idea that could save people from headaches about
backward compatibility.
It could be specified as a special dependency, like ext-async.

I’m not sure why the To/CC list got so long. Please reply to the list, not to each person…

On Fri, Nov 21, 2025, at 14:45, Edmond Dantes wrote:

Hello.

To use AMPHP you don’t need to run its server. But let’s simplify the situation.

You have Fiber.
With Fiber you can build an EventLoop + Scheduler, create two
coroutines, and then run code inside them that, for example, renders a
template.

It will require a fair amount of code, but it’s possible.
At the same time, ob_start/ob_end will immediately break, because they
don’t support Fiber switching. This is a known bug.
And if you pass global state into closures, and that state changes
unpredictably, the application logic will essentially break.

I don’t know why you’d build all of that from scratch. https://revolt.run/ will allow you to run amphp/reactphp in any request. It “just works”

You do need to consciously kick off the event loop, though, as well as install some specific extensions, or you’re stuck with the stream_select implementation, which isn’t the best.

That being said, the only problem you’d run into trying to install this in a WordPress plugin is if any other plugin is using Fibers or another Fiber library. From experience, they don’t always play nicely together, and there should only be one event loop running at a time.

What’s different with TrueAsync

  1. You don’t need to write a lot of code. Yes, just spawn();
  2. ob_start and ob_end will work correctly.
  3. If closures share a variable that multiple coroutines write to
    unpredictably, the application will still break.

Without well-defined suspension points, literally all code is unpredictable. This is what people have been trying to tell you for the last several weeks. How can I know when the code will yield to the scheduler? How do I reason about it? The foundation is there in the RFC and the code, but it needs more definition and guarantees.

In other words, no matter how asynchrony is implemented in PHP (and
other langs), it will always require correct handling of shared
memory.
And I want to clarify a bit. This is not about a global variable. It’s
about shared memory. These are not the same things. A local variable
can be passed into a closure by reference.
But if you don’t call WP() inside a coroutine, nothing prevents you
from using asynchrony.

PHP is full of shared memory, even applications. Take, for example:

$val = $this->cache->get(‘key’) ?? $this->cache->set(‘key’, ‘value’);

Currently in PHP, this is unsafe if the cache is a proper cache (like memcached/redis), but for an in-memory implementation, it’s effectively a Check-And-Set. If the cache is implemented as async, then it becomes unsafe even for in-memory implementations. This may-or-may-not be desired.

However, this gets weird, very quickly, very fast. $_SERVER/$_REQUEST/$_ENV/etc are all global memory that usually doesn’t change. However, frameworks change it all the time, to inject environment variables from .env files, to sanitise inputs, etc. Swoole avoids this by having a Request object. This is simply not present in PHP.

Global state is a core part of PHP, like it or not.

— Rob