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

On Sat, Nov 22, 2025, at 14:55, Edmond Dantes wrote:

So I guess you want to use spawn() in a similar way as call_user_func() works.
yes

This changes the behavior of file_get_contents() from the outside
No.

function file_get_contents(string $filename): string
{
$fh = fopen();

// It creates an EPOLL event so it can wake us when the data
becomes available.
$event = [ReactorAPI.create](http://ReactorAPI.create)_event_from($fh);
$waker = [Scheduler.getCurrentWaker](http://Scheduler.getCurrentWaker)();
// Event Driven logic inside.
$[waker.add](http://waker.add)_event($event, function() use($waker) {
// Wakeup this coroutine
$[waker.wake](http://waker.wake)();
});

// suspend current coroutine
// zz..... z.....
[Scheduler.suspend](http://Scheduler.suspend)();

// Continue here after the IO event

// Now we have date, return
return fread($fh, ....);
}

This is pseudocode. You can assume it always works.
If you call file_get_contents directly, it behaves the same way.
So it does not matter where file_get_contents is called.
Since all PHP code together with TrueAsync runs inside coroutines,
file_get_contents will suspend the coroutine in which it was invoked.

When you call spawn, you simply run the function in another
coroutine, not in your own. But spawn has no effect on
file_get_contents.

We’re not at risk of DataRace yet :slight_smile: We don’t have multithreading.
And most likely it won’t appear anytime soon.

We are in data-race territory though:

spawn(fn() => file_put_contents(‘file’, ‘long string’))
spawn(fn() => file_get_contents(‘file’))

I’m on a phone, so I’m not sure I got all the syntax right, but hopefully my intent is clear. But if these run “concurrently” the scheduler will theoretically batch the reading/writing of bytes in an interleaved way, causing absolute chaos and corruption.

The same thing would happen with db drivers that typically use a token to keep track of which response goes to which request (I maintain an async db driver for amphp). There will be a need to ensure the stream cannot be interleaved with other coroutines. I can do this with amphp locks, but there isn’t even a semaphore implementation to build a lock around.

— Rob

Edmond Dantes <edmond.ht@gmail.com> hat am 22.11.2025 14:55 CET geschrieben:

> So I guess you want to use spawn() in a similar way as call_user_func() works.
yes

> This changes the behavior of file_get_contents() from the outside
No.

function file_get_contents(string $filename): string
{
    $fh = fopen();

    // It creates an EPOLL event so it can wake us when the data
becomes available.
     $event = ReactorAPI.create_event_from($fh);
    $waker = Scheduler.getCurrentWaker();
    // Event Driven logic inside.
    $waker.add_event($event, function() use($waker) {
          // Wakeup this coroutine
          $waker.wake();
     });

    // suspend current coroutine
    // zz..... z.....
    Scheduler.suspend();

    // Continue here after the IO event

    // Now we have date, return
    return fread($fh, ....);
}

This is pseudocode. You can assume it always works.
If you call `file_get_contents` directly, it behaves the same way.
So it does not matter where `file_get_contents` is called.
Since all PHP code together with TrueAsync runs inside coroutines,
`file_get_contents` will suspend the coroutine in which it was invoked.

When you call `spawn`, you simply run the function in another
coroutine, not in your own. But `spawn` has no effect on
`file_get_contents`.

We’re not at risk of DataRace yet :slight_smile: We don’t have multithreading.
And most likely it won’t appear anytime soon.

    // Continue here after the IO event

From my understanding, the code does not continue if there is no io event? Will it use default_socket_timeout from php.ini and/or use the timeout specified in the stream context?

Can I mix sync IO and async IO in one function? e.g. if the server uses a mixed storage of SSDs and HDDs and I only want async io for the SSDs?

Best Regards
Thomas

From my understanding, the code does not continue if there is no io event?

Yes

Will it use default_socket_timeout from php.ini and/or use the timeout specified in the stream context?

There are many more different cases in the real C code.
For sockets, I remember it uses a timeout.
For filesystem files, I think there is no such thing.

Can I mix sync IO and async IO in one function?

In essence, there is no more sync I/O.
Any input/output is potentially considered asynchronous.
Launching multiple coroutines lets the programmer create several I/O
operations that run asynchronously.
Thus, the degree of asynchrony is equal to the number of coroutines.

The code inside a coroutine creates the illusion of step-by-step
sequential execution with no extra effort.
Therefore, in this model the programmer writes less code and uses
Promises less often.

if the server uses a mixed storage of SSDs and HDDs and I only want async io for the SSDs?

How will PHP understand which type of storage it is dealing with?

But if these run “concurrently” the scheduler will theoretically batch the reading/writing of bytes in an interleaved way, causing absolute chaos and corruption.

Yes of course, that’s exactly what will happen.

Can I mix sync IO and async IO in one function?

To be more precise, the idea is that ideally all input and output must
be non blocking and must work through an EventLoop. In that case an
application that performs I O often receives the maximum benefit from
coroutines. From the PHP process point of view the application is
always asynchronous. From the coroutine point of view it is
synchronous.

The thinking of a programmer who works with such code is different
from JavaScript. Here I do not think about which code will be
asynchronous, I think about how many functions I can run in the
background independently of each other. Because all code is already
asynchronous. The only question is how many virtual logical threads I
want to divide it into. This philosophy is better for business logic
code but worse for systems programming. That is why I chose it.

use Async\spawn;
use Async\Channel;

function worker(Channel $tasks) {
    // Start a process for this coroutine
    $proc = proc_open("php worker.php", [
        ["pipe","r"],
        ["pipe","w"],
        ["pipe","w"]
    ], $pipes);

    $stdin  = $pipes[0];
    $stdout = $pipes[1];

    while (true) {
        // Receive a task (this suspends the coroutine)
        $task = $tasks->recv();

        // Write task to process (suspends while writing)
        fwrite($stdin, $task . "\n");

        // Read result (suspends while waiting for data)
        $result = fgets($stdout);
        ... make some else
    }
}

$tasks = new Channel();

// Start N workers
for ($i = 0; $i < 10; $i++) {
    spawn(worker(...), $tasks);
}

// Producer...
$tasks->push(...);
$tasks->push(...);
$tasks->push(...);

// Close Channel auto after leave function

Please look at this code.
There are three or five other ways to implement it. And most of them
would likely be worse in terms of reliability. Because in this example
there is almost no asynchronous code at all. Everything is sequential.
There is no synchronization. There is nothing. Just a channel and just
coroutines. That is all. And inside each coroutine the code is
synchronous.

And you do not even need any structural concurrency. The task is
solved because when the channel is destroyed, an exception is thrown
and the coroutines will terminate together with the rest of the code.

Of course, if you add automatic process restart, it becomes a bit more
complex, but the code is still synchronous. Which is very good.

Imagine a person who has never worked with async before. How hard
would it be for them to write this code?

---
Ed

I've been thinking about this quite a bit and I'm still not quite sure
we need a feature flag to enable async behavior.

@Ed Unless something calls `spawn` all I/O is going to be blocking &
non-concurrent, correct?

@Deleu Since you currently can't use `spawn`; no libraries have it and
therefore if you're a library developer and you implement async
behavior after this feature has been released you'll probably release
a new major version of your library signalling the new version is not
backwards compatible. Currently, if you require a new library you need
to read the documentation to see how it works as well...

Also, I'm not quite sure what we would want to happen if async is
disabled but you're requiring a library that spawns coroutines. Surely
the library won't work as designed?

Best Regards,
Bart Vanhoutte

Op za 22 nov 2025 om 17:01 schreef Edmond Dantes <edmond.ht@gmail.com>:

use Async\spawn;
use Async\Channel;

function worker(Channel $tasks) {
    // Start a process for this coroutine
    $proc = proc_open("php worker.php", [
        ["pipe","r"],
        ["pipe","w"],
        ["pipe","w"]
    ], $pipes);

    $stdin  = $pipes[0];
    $stdout = $pipes[1];

    while (true) {
        // Receive a task (this suspends the coroutine)
        $task = $tasks->recv();

        // Write task to process (suspends while writing)
        fwrite($stdin, $task . "\n");

        // Read result (suspends while waiting for data)
        $result = fgets($stdout);
        ... make some else
    }
}

$tasks = new Channel();

// Start N workers
for ($i = 0; $i < 10; $i++) {
    spawn(worker(...), $tasks);
}

// Producer...
$tasks->push(...);
$tasks->push(...);
$tasks->push(...);

// Close Channel auto after leave function

Please look at this code.
There are three or five other ways to implement it. And most of them
would likely be worse in terms of reliability. Because in this example
there is almost no asynchronous code at all. Everything is sequential.
There is no synchronization. There is nothing. Just a channel and just
coroutines. That is all. And inside each coroutine the code is
synchronous.

And you do not even need any structural concurrency. The task is
solved because when the channel is destroyed, an exception is thrown
and the coroutines will terminate together with the rest of the code.

Of course, if you add automatic process restart, it becomes a bit more
complex, but the code is still synchronous. Which is very good.

Imagine a person who has never worked with async before. How hard
would it be for them to write this code?

---
Ed

Hi,

On Wed, Nov 19, 2025 at 1:37 PM Edmond Dantes <edmond.ht@gmail.com> wrote:

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: https://wiki.php.net/rfc/true_async/voting
RFC https://wiki.php.net/rfc/true_async

The vote officially starts tomorrow, as previously announced.

Just note here that you effectively started the vote on Nov 19th (votes were already coming in so that should be the official start). There were changes to the policy that were merged on Nov 20th so they should apply from that date. The current text is following: https://github.com/php/policies/blob/c2a1c602deee3988a9ce9cb740169973f5f4a781/feature-proposals.rst . If it was started after that merge, this RFC would be automatically invalid (there wasn’t proper pre-announcement and so on) but as it started before, I think it’s still a valid vote. It means that you have only 3 days to either stop the vote or you will need to let it finish. If it fails, you won’t be able to propose this for for another 6 months unless significant changes are done. I guess it will need significant changes anyway but just wanted to let you know what the options are here in terms of stopping the vote.

Kind regards,

Jakub

Hi

On 11/23/25 17:59, Jakub Zelenka wrote:

invalid (there wasn't proper pre-announcement and so on) but as it started
before, I think it's still a valid vote. It means that you have only 3 days

As the author of the RFC amending the policy, I agree. While there was some confusion with regard to the start of the vote (and the placement on a separate page vs the RFC page) it was a valid vote with regard to the previous policy and as such is “grandfathered” in.

Vote cancellation will need to follow the new policy, though.

Best regards
Tim DĂźsterhus

Hello.

To stop the vote, do I need to change the status?

вс, 23 нояб. 2025 г., 18:59 Jakub Zelenka <bukka@php.net>:

Hi,

On Wed, Nov 19, 2025 at 1:37 PM Edmond Dantes <edmond.ht@gmail.com> wrote:

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: https://wiki.php.net/rfc/true_async/voting
RFC https://wiki.php.net/rfc/true_async

The vote officially starts tomorrow, as previously announced.

Just note here that you effectively started the vote on Nov 19th (votes were already coming in so that should be the official start). There were changes to the policy that were merged on Nov 20th so they should apply from that date. The current text is following: https://github.com/php/policies/blob/c2a1c602deee3988a9ce9cb740169973f5f4a781/feature-proposals.rst . If it was started after that merge, this RFC would be automatically invalid (there wasn’t proper pre-announcement and so on) but as it started before, I think it’s still a valid vote. It means that you have only 3 days to either stop the vote or you will need to let it finish. If it fails, you won’t be able to propose this for for another 6 months unless significant changes are done. I guess it will need significant changes anyway but just wanted to let you know what the options are here in terms of stopping the vote.

Kind regards,

Jakub

Hello all.

The voting process has been stopped.

Thanks to all.

---
Ed

Hello.

@Ed Unless something calls `spawn` all I/O is going to be blocking &
non-concurrent, correct?

Yes.
If no one calls spawn, this is equivalent to the code running inside a
single coroutine.

At the moment, TrueAsync has an internal flag that allows it to be
enabled or disabled. If Async is disabled, an exception will be
thrown.
So essentially this capability already exists in the code, and the
global flag that turns the feature on or off was inherited from the
early versions of the library.

Hello.

Bob Weinand shared an excellent idea about how Fiber can be used
together with coroutines without breaking backward compatibility.

It is enough to automatically assign a coroutine to a fiber, and then
the code inside the fiber will be normalized relative to the
Scheduler.
Fiber receives the same properties as a coroutine and can be used as a
stackful generator.

This change makes it possible not to block Fiber, but instead allow it
to work together with TrueAsync and be used in cases where stackful
generators are needed.
A Fiber blocks the execution of the coroutine from which it was
started, meaning its symmetric behavior is preserved.

Respect to Bob Weinand

---
Ed

Hi

On 11/23/25 19:57, Edmond Dantes wrote:

The voting process has been stopped.

I moved the RFC back to “Under Discussion” in the overview: PHP: rfc

Best regards
Tim DĂźsterhus

On 23/11/2025 19:09, Edmond Dantes wrote:

@Ed Unless something calls `spawn` all I/O is going to be blocking &
non-concurrent, correct?

Yes.
If no one calls spawn, this is equivalent to the code running inside a
single coroutine.

At the moment, TrueAsync has an internal flag that allows it to be
enabled or disabled. If Async is disabled, an exception will be
thrown.

I think there's a lot of confusion in this thread because different people are talking about different scenarios. Perhaps it would be useful to introduce some User Stories...

Async Alice is working on a brand new application written in PHP 9, and is designing it from the ground up to make use of async capabilities wherever possible. She wants third-party libraries to use async I/O so that she can use them in her design.

Beginner Bob has a recently built application, and thinks there's an opportunity to improve it with async I/O, but doesn't know anything about it. He wants a simple-to-use API that lets him get the benefits, and clear instructions on what pitfalls to look out for.

Legacy Les is maintaining a 20-year-old business back office system, which makes extensive use of global state and does not have good automated testing. He wants to run it under PHP 9, and to use up-to-date third-party libraries for new functionality, without an expensive and risky rewrite of existing code.

Finally, SDK Susie is publishing the official PHP library for a popular cloud API. She wants to serve the best version she can for Alice, Bob, and Les, but doesn't want to maintain separate "sync" and "async" branches of the library or its methods.

Feel free to create more personas if you want to talk about additional scenarios.

The first thing I want to clarify is that SDK Susie doesn't necessarily need to change the public methods of her library; she can still use async I/O internally. For instance, if a method already returns an Iterator to silently fetch a page of results at a time, that can be changed to store Promises internally, and await them when the data is needed.

However, she might want to mark it as a breaking change anyway, so that Async Alice and Beginner Bob know they are opting into it.

Legacy Les won't get it until he opts in, but at some point he will need a new version of the library for other reasons (e.g. because the cloud API becomes incompatible with the old library version); so he still needs a way to run it safely.

If he runs PHP in a mode where any attempt to use async I/O *throws an error*, he still can't use the new version of the library, so this doesn't help him.

However, if Legacy Les can run PHP in a mode where any attempt to use async I/O is *automatically run synchronously*, then he will be happy: he can run his legacy application under PHP 9, and use the updated library, without worrying about async code.

Beginner Bob doesn't want to run his whole application in "sync only" mode, but might want to switch *parts of it*, so that he doesn't have to think about them yet. So a scoped, rather than global, switch might be useful for him.

This is how I picture that mode working: when SDK Susie's library code calls "spawn", a Coroutine is created as normal. However, when it suspends, the Scheduler immediately resumes it, rather than switching to a different Coroutine. The library code will see the Coroutine object it expects, but passing it to "await" will immediately produce its result.

However, I might well be misunderstanding something, and this is either impossible or difficult to implement. If so, I think some other solution to Legacy Les's requirements is needed.

I hope this description is useful.

--
Rowan Tommins
[IMSoP]

Hello,

I have serious concerns about any approach that may cause the event loop to block. The correct ways to handle this should not involve blocking the loop — whether globally or in a scoped manner — while attempting to let synchronous and asynchronous code coexist in the same environment. More appropriate alternatives would be offloading execution to another thread (which is far too complex for the current VM and ecosystem) or relying on preemption (which is not feasible to implement here).

In addition, the cascading effects of such an approach are significant: libraries would be forced to implement internal mechanisms just to deal with unexpected loop stalls and similar issues.

It would be much healthier for new libraries to be designed specifically for the asynchronous environment, in the same way Laravel created Octane to run on top of Swoole.

Kind regards,
LuĂ­s VinĂ­cius.

Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> escreveu (domingo, 23/11/2025 Ă (s) 18:58):

On 23/11/2025 19:09, Edmond Dantes wrote:


> ~~~
> @Ed Unless something calls `spawn` all I/O is going to be blocking &
> non-concurrent, correct?
> 
> ~~~

```
Yes.
If no one calls spawn, this is equivalent to the code running inside a
single coroutine.

At the moment, TrueAsync has an internal flag that allows it to be
enabled or disabled. If Async is disabled, an exception will be
thrown.
```

I think there’s a lot of confusion in this thread because different people are talking about different scenarios. Perhaps it would be useful to introduce some User Stories…

Async Alice is working on a brand new application written in PHP 9, and is designing it from the ground up to make use of async capabilities wherever possible. She wants third-party libraries to use async I/O so that she can use them in her design.

Beginner Bob has a recently built application, and thinks there’s an opportunity to improve it with async I/O, but doesn’t know anything about it. He wants a simple-to-use API that lets him get the benefits, and clear instructions on what pitfalls to look out for.

Legacy Les is maintaining a 20-year-old business back office system, which makes extensive use of global state and does not have good automated testing. He wants to run it under PHP 9, and to use up-to-date third-party libraries for new functionality, without an expensive and risky rewrite of existing code.

Finally, SDK Susie is publishing the official PHP library for a popular cloud API. She wants to serve the best version she can for Alice, Bob, and Les, but doesn’t want to maintain separate “sync” and “async” branches of the library or its methods.

Feel free to create more personas if you want to talk about additional scenarios.

The first thing I want to clarify is that SDK Susie doesn’t necessarily need to change the public methods of her library; she can still use async I/O internally. For instance, if a method already returns an Iterator to silently fetch a page of results at a time, that can be changed to store Promises internally, and await them when the data is needed.

However, she might want to mark it as a breaking change anyway, so that Async Alice and Beginner Bob know they are opting into it.

Legacy Les won’t get it until he opts in, but at some point he will need a new version of the library for other reasons (e.g. because the cloud API becomes incompatible with the old library version); so he still needs a way to run it safely.

If he runs PHP in a mode where any attempt to use async I/O throws an error, he still can’t use the new version of the library, so this doesn’t help him.

However, if Legacy Les can run PHP in a mode where any attempt to use async I/O is automatically run synchronously, then he will be happy: he can run his legacy application under PHP 9, and use the updated library, without worrying about async code.

Beginner Bob doesn’t want to run his whole application in “sync only” mode, but might want to switch parts of it, so that he doesn’t have to think about them yet. So a scoped, rather than global, switch might be useful for him.

This is how I picture that mode working: when SDK Susie’s library code calls “spawn”, a Coroutine is created as normal. However, when it suspends, the Scheduler immediately resumes it, rather than switching to a different Coroutine. The library code will see the Coroutine object it expects, but passing it to “await” will immediately produce its result.

However, I might well be misunderstanding something, and this is either impossible or difficult to implement. If so, I think some other solution to Legacy Les’s requirements is needed.

I hope this description is useful.

-- 
Rowan Tommins
[IMSoP]

On Sun, 23 Nov 2025 at 21:29 LuĂ­s VinĂ­cius Santos da Costa Barros <luisvscbarros@gmail.com> wrote:

Hello,

I have serious concerns about any approach that may cause the event loop to block. The correct ways to handle this should not involve blocking the loop — whether globally or in a scoped manner — while attempting to let synchronous and asynchronous code coexist in the same environment. More appropriate alternatives would be offloading execution to another thread (which is far too complex for the current VM and ecosystem) or relying on preemption (which is not feasible to implement here).

In addition, the cascading effects of such an approach are significant: libraries would be forced to implement internal mechanisms just to deal with unexpected loop stalls and similar issues.

It would be much healthier for new libraries to be designed specifically for the asynchronous environment, in the same way Laravel created Octane to run on top of Swoole.

If I could summarize months of discussion on this RFC, the primary topic would be the consensus that we do not want to have a split in the community. If every PHP package needs an adapter to be async compatible that essentially means the efforts to not break the community in half has failed.

A different way to look at it may be that it’s not so much about blocking the event loop, but rather not even having an event loop in the first place (in case you’re running sync-PHP). And if you’re partially running async at an isolated area, then once that returns and all coroutines are finished the event loop may also finish and we’re back to sync.

Hello.

I think there's a lot of confusion in this thread because different people are talking about different scenarios. Perhaps it would be useful to introduce some User Stories

Yesterday I briefly reviewed the WordPress code to estimate the
refactoring effort.
The amount of global state accessed through `global` and `static` is
quite large.
However, it seems technically possible to switch global and static
variables per coroutine.
I’m not sure how this will affect performance (maybe 1–2% on
switches?), but the task looks realistic.

At first I thought this was a bad approach, because if such a feature
were added to PHP,
everyone would start using `global` as a way to exchange data inside a
coroutine. However, this situation can be viewed from a completely
different perspective:

function myFunction(): void {
    global $x;
}

// This code is equivalent to:
function myFunction(): void {
    effect ?string $x = null; // <-- function effect, external dependency
}

// This code is equivalent to:
function myFunction(): void in ?string $x {

}

// Then calling the function:
with ($x = "string") {
    myFunction();
}

In other words, this is a function effect that turns previously
unmanaged and unsafe code into a function with an explicit
external-dependency contract.
And in the future, it could be improved by adding proper effect
support to the language. It turns out that a feature previously seen
as an antipattern can actually be framed as a modern concept from
functional programming.

Of course, these statements need to be validated with code and testing,
and they also require a separate RFC.

I forgot to add an important point.
If a coroutine can be started with its own separate Globals state,
this means there is a technical possibility to safely run coroutines
in applications with global state, guaranteeing they won’t break
anything.

If the rule of having zero Globals becomes the default rule for
starting coroutines, it makes PHP potentially less dangerous.
It means that data can be passed into a coroutine only explicitly at
creation time, and in no other way.

Of course, we need to consider the potential performance impact of
this logic. But if the performance drop is minimal, this feature could
potentially make the language better and essentially turn PHP into a
language where global state doesn’t exist at all. \
After all, any code is code running inside a coroutine.
That means `$GLOBALS` effectively no longer exists. Memory is always
localized to the execution context.

This property potentially allows coroutines to be moved between
threads in the future, because the memory model begins to look like an
isolated region that belongs to the coroutine.

At the same time, this property **does not break existing code**,
because everything remains 100% functional inside a coroutine.
But now we gain the ability to run multiple instances of WordPress in
a single PHP thread, if we adjust the startup code.

All of this resembles Erlang and sounds a bit crazy for a PHP
developer, but so far I haven’t been able to come up with a
counterexample that would make this property negative.

Let me repeat the conclusions:

1. Each coroutine has its own GLOBALS/Static, and a developer cannot
pass global state into a coroutine.
2. This forces the developer to pass objects explicitly, because there
are simply no other mechanisms available.
3. `static` caching breaks for coroutines. But since coroutines don’t
currently exist, this is not a problem. Developers will just need to
account for it.

On 24 November 2025 08:56:54 GMT, Edmond Dantes <edmond.ht@gmail.com> wrote:

3. `static` caching breaks for coroutines. But since coroutines don’t
currently exist, this is not a problem. Developers will just need to
account for it.

This is exactly the kind of ambiguous statement I was trying to avoid by coming up with persona names and user stories - *which* developers will need to account for it?

Will Legacy Les see a side effect when he upgrades to SDK Susie's new async library?

Rowan Tommins
[IMSoP]