PHP True Async RFC Stage 4

php.internals

Edmond Dantes

331 days ago
Good day, everyone. I hope you're doing well. I’m happy to present the fourth version of the RFC. It wasn’t just me who worked on it — members of the PHP community contributed as well. Many thanks to everyone for your input! https://wiki.php.net/rfc/true_async **What has changed in this version?** The RFC has been significantly simplified: 1. Components (such as TaskGroup) that can be discussed in separate RFCs have been removed from the current one. 2. Coroutines can now be created anywhere — even inside shutdown_function. 3. Added Memory Management and Garbage Collection section Although work on the previous API RFC was interrupted and we weren’t able to include it in PHP 8.5, it still provided valuable feedback on the Async API code. During this time, I managed to refactor and optimize the TrueAsync code, which showed promising performance results in I/O scenarios. A test integration between **NGINX UNIT** and the **TrueAsync API** was implemented to evaluate the possibility of using PHP as an asynchronous backend for a web server: https://github.com/EdmondDantes/nginx-unit/tree/true-async/src/true-async-php During this time, the project has come very close to beta status. Once again, I want to thank everyone who supported me during difficult times, offered advice, and helped develop this project. Given the maturity of both the code and the RFC, this time I hope to proceed with a vote. Wishing you all a great day, and thank you for your feedback!

Adam Cable

331 days ago
On Sun, Oct 5, 2025 at 7:51 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Good day, everyone. I hope you're doing well. > > I’m happy to present the fourth version of the RFC. It wasn’t just me > who worked on it — members of the PHP community contributed as well. > Many thanks to everyone for your input! > > https://wiki.php.net/rfc/true_async > > **What has changed in this version?** > > The RFC has been significantly simplified: > > 1. Components (such as TaskGroup) that can be discussed in separate > RFCs have been removed from the current one. > 2. Coroutines can now be created anywhere — even inside shutdown_function. > 3. Added Memory Management and Garbage Collection section > > Although work on the previous API RFC was interrupted and we weren’t > able to include it in PHP 8.5, it still provided valuable feedback on > the Async API code. > > During this time, I managed to refactor and optimize the TrueAsync > code, which showed promising performance results in I/O scenarios. > > A test integration between **NGINX UNIT** and the **TrueAsync API** > was implemented to evaluate the possibility of using PHP as an > asynchronous backend for a web server: > > https://github.com/EdmondDantes/nginx-unit/tree/true-async/src/true-async-php > > During this time, the project has come very close to beta status. > > Once again, I want to thank everyone who supported me during difficult > times, offered advice, and helped develop this project. > > Given the maturity of both the code and the RFC, this time I hope to > proceed with a vote. > > Wishing you all a great day, and thank you for your feedback! >
Hi, I am so looking forward to this capability! Just a quick question - other methods that tried to provide async/parallel type functionality previously were only available via the CLI. I can see a big opportunity for people running websites with Apache + PHP-FPM where on each page request you do stuff like: Call API 1 (e.g. external auth component) Call API 2 (e.g. product catalogue) Call API 3 (e.g. setup payment processor) Am hoping that you could put these three calls within a Scope and therefore have all three calls run at the same time, and only have to wait as long as the slowest API, rather than the combination of all 3 response times. I didn't see anything in the RFC about this, so just wanted to check. Thanks, Adam

Edmond Dantes

330 days ago
Hello.
> Just a quick question - other methods that tried to provide async/parallel type functionality previously were only available via the CLI.
TrueAsync itself is integrated into PHP in such a way that it is always active. The scenario you described is technically possible (Of course, this can also be useful for sending telemetry in a way that doesn’t interfere with request processing.), but it’s not particularly relevant in the context of modern development. Why? Because client requests are usually processed sequentially, step by step. Parallel tasks are rare. Therefore, from the server’s perspective, the main benefit of concurrency is the ability to handle multiple requests within a single process. The same thing that Swoole, AMPHP, and other modern backend solutions do. And this is one of the reasons why FPM is morally outdated and therefore not used in stateful backends. That’s why you encounter CLI so often.

Larry Garfield

330 days ago
On Mon, Oct 6, 2025, at 12:34 AM, Edmond Dantes wrote:
> Hello. > >> Just a quick question - other methods that tried to provide async/parallel type functionality previously were only available via the CLI. > > TrueAsync itself is integrated into PHP in such a way that it is > always active. The scenario you described is technically possible (Of > course, this can also be useful for sending telemetry in a way that > doesn’t interfere with request processing.), but it’s not particularly > relevant in the context of modern development. > > Why? > > Because client requests are usually processed sequentially, step by > step. Parallel tasks are rare.
This is simply not true. The example you're replying to is quite common. It's even more common for the database. WordPress, Drupal, and many other such systems frequently run different DB queries to build different components of a page. (Blocks, widgets, components, the names differ.) Being able to do those in parallel is a natural optimization that we were thinking about in Drupal nearly 15 years ago, but it wasn't viable at the time.
> Therefore, from the server’s > perspective, the main benefit of concurrency is the ability to handle > multiple requests within a single process.
That is *A benefit*. It is not the *only benefit*. Being able to compress the time of each request in a shared-nothing model is absolutely valuable. Remember, in the wild, PHP-FPM and mod_php are by orders of magnitude the most common ways PHP is executed. React, Swoole, etc. are rounding errors in most of the market. And the alternate runtime with the most momentum is FrankenPHP, which reuses processes but is still "one request in a process at a time."
> The same thing that Swoole, > AMPHP, and other modern backend solutions do. > > And this is one of the reasons why FPM is morally outdated and
I am going to assume this is a translation issue, because "morally outdated" is the wrong term here. "Morally outdated" is how you'd describe "racial segregation is good, actually." Not "this technology is slower than we need it to be." You probably mean "severely outdated" or something along those lines. Which, as I explained above, is simply not true. PHP is going to be running in a mostly shared-nothing environment for the foreseeable future. Those use cases still would benefit from async support. --Larry Garfield

Edmond Dantes

330 days ago
Hi.
> This is simply not true. The example you're replying to is quite common.
It’s probably my poor English. So I’ll try to rephrase the idea: The majority of database queries are executed sequentially, step by step. Not all queries. Not always. But most of them. This is true even in languages that already have async.
> That is *A benefit*. It is not the *only benefit*. Being able to compress the time of each request in a shared-nothing model is absolutely valuable.
(It’s important not to overestimate this model, otherwise lately you sometimes hear complaints that the ultra-trendy immutable philosophy leads to terrible performance :)) A stateful worker does not automatically mean active sharing of state between requests. It gives the developer the choice of what can and cannot be shared. You have a choice. If you want all services to follow the immutable model — you can do that. But now you don’t have to pay for compilation or initialization. You have complete creative freedom.
> Remember, in the wild, PHP-FPM and mod_php are by orders of magnitude the most common ways PHP is executed. React, Swoole, etc. are rounding errors in most of the market. And the alternate runtime with the most momentum is FrankenPHP, which reuses processes but is still "one request in a process at a time."
Almost no one wants to spend time building code with a technology that isn’t supported. So when people want to do things like that, they simply choose another language. I’m not saying that async isn’t supported in CGI mode... but.. it’s just that a gain of a few milliseconds is unlikely to be noticeable.
> I am going to assume this is a translation issue, because "morally outdated" is the wrong term here.
Thank you! That’s true. But a more accurate translation would be: it’s a technology that has become outdated not because of time, but because the circumstances and requirements have changed. Back in the years when CGI was evolving, things were different. There were no servers with a dozen cores.

Deleu

330 days ago
Hi! On Mon, Oct 6, 2025 at 1:43 PM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hi. > > This is simply not true. The example you're replying to is quite common. > > It’s probably my poor English. So I’ll try to rephrase the idea: > The majority of database queries are executed sequentially, step by > step. Not all queries. Not always. But most of them. > This is true even in languages that already have async. >
I find this a bit confusing to contextualize. I agree that most code written was probably written following the principle of 1-query-at-a-time, even in languages that already support async. But at the same time, if you're tasked with optimizing the time it takes for a certain HTTP Endpoint to execute then caching data OR rethinking query execution flow are among the top contenders for change. What I'm trying to say is that I would look at this from a different lensis. You're right that just because async is already available doesn't mean that queries will take advantage of it by default. But for the critical parts of a system that requires optimization of the execution duration, having async capabilities can easily drive the decision of how the code will be restructured to fulfill the need for performance improvements.
> > > That is *A benefit*. It is not the *only benefit*. Being able to > compress the time of each request in a shared-nothing model is absolutely > valuable. > (It’s important not to overestimate this model, otherwise lately you > sometimes hear complaints that the ultra-trendy immutable philosophy > leads to terrible performance :)) > > A stateful worker does not automatically mean active sharing of state > between requests. It gives the developer the choice of what can and > cannot be shared. You have a choice. If you want all services to > follow the immutable model — you can do that. But now you don’t have > to pay for compilation or initialization. You have complete creative > freedom. >
Talking about stateful workers and shared-state here is a bit ambiguous, at least for me, tbh. When you say the developer has a choice, my interpretation is that you mean to say that the PHP Developer can choose what to share and what not to share by defining static variables, much like most other languages implement the Singleton Pattern. In PHP, especially with the share-nothing model, even static variables are cleared out. While there's no denying that there is value in creating a shareable space for performance gains, and this is seen in popularization of Swoole, Laravel Octane, FrankenPHP Worker Mode, etc; there's still a point to be made about the fact that 30 years worth of PHP code exists in the wild assuming that static variables gets cleared out between requests and as such are a non-trivial task to port them to newer execution models. This is where I think Larry's point comes strong with the fact that most these new "modern / non-legacy" execution models are just a rounding error in the amount of PHP code being executed everyday and where support of async execution for FPM would be a game changer for code that is too hard to lift-and-shift into worker mode, but not so hard to make adjustments in the next PHP upgrade to e.g. parallelize database queries.
> > Remember, in the wild, PHP-FPM and mod_php are by orders of magnitude > the most common ways PHP is executed. React, Swoole, etc. are rounding > errors in most of the market. And the alternate runtime with the most > momentum is FrankenPHP, which reuses processes but is still "one request in > a process at a time." > > Almost no one wants to spend time building code with a technology that > isn’t supported. So when people want to do things like that, they > simply choose another language. > I’m not saying that async isn’t supported in CGI mode... but.. > it’s just that a gain of a few milliseconds is unlikely to be noticeable. >
If you have a report that executes 3 queries and each query averages between 4 to 5 seconds, this report takes up to 15 seconds to run in PHP. The capability of executing async code in FPM would mean a 3x performance gain on a report like this. That is far from just a few milliseconds gain. And to be honest, the biggest gain for me would be the ability to keep the applications contextual logic within a single execution unit. One very common route that is taken with today's options is to break those 3 queries into separate HTTP endpoints and let the frontend stitch them together which provides a very similar performance gain by taking advantage of JS/Browser parallel requests since PHP is unable to do so.
-- Marco Deleu

Unnamed Person

330 days ago
> Deleu <deleugyn@gmail.com> hat am 06.10.2025 19:29 CEST geschrieben: > > > > Hi! > > > On Mon, Oct 6, 2025 at 1:43 PM Edmond Dantes <edmond.ht@gmail.com <mailto:edmond.ht@gmail.com>> wrote: > > Hi. > > > This is simply not true. The example you're replying to is quite common. > > > > It’s probably my poor English. So I’ll try to rephrase the idea: > > The majority of database queries are executed sequentially, step by > > step. Not all queries. Not always. But most of them. > > This is true even in languages that already have async. > > I find this a bit confusing to contextualize. I agree that most code written was probably written following the principle of 1-query-at-a-time, even in languages that already support async. But at the same time, if you're tasked with optimizing the time it takes for a certain HTTP Endpoint to execute then caching data OR rethinking query execution flow are among the top contenders for change. What I'm trying to say is that I would look at this from a different lensis. You're right that just because async is already available doesn't mean that queries will take advantage of it by default. But for the critical parts of a system that requires optimization of the execution duration, having async capabilities can easily drive the decision of how the code will be restructured to fulfill the need for performance improvements. > > > > That is *A benefit*. It is not the *only benefit*. Being able to compress the time of each request in a shared-nothing model is absolutely valuable. > > (It’s important not to overestimate this model, otherwise lately you > > sometimes hear complaints that the ultra-trendy immutable philosophy > > leads to terrible performance :)) > > > > A stateful worker does not automatically mean active sharing of state > > between requests. It gives the developer the choice of what can and > > cannot be shared. You have a choice. If you want all services to > > follow the immutable model — you can do that. But now you don’t have > > to pay for compilation or initialization. You have complete creative > > freedom. > > Talking about stateful workers and shared-state here is a bit ambiguous, at least for me, tbh. When you say the developer has a choice, my interpretation is that you mean to say that the PHP Developer can choose what to share and what not to share by defining static variables, much like most other languages implement the Singleton Pattern. In PHP, especially with the share-nothing model, even static variables are cleared out. While there's no denying that there is value in creating a shareable space for performance gains, and this is seen in popularization of Swoole, Laravel Octane, FrankenPHP Worker Mode, etc; there's still a point to be made about the fact that 30 years worth of PHP code exists in the wild assuming that static variables gets cleared out between requests and as such are a non-trivial task to port them to newer execution models. This is where I think Larry's point comes strong with the fact that most these new "modern / non-legacy" execution models are just a rounding error in the amount of PHP code being executed everyday and where support of async execution for FPM would be a game changer for code that is too hard to lift-and-shift into worker mode, but not so hard to make adjustments in the next PHP upgrade to e.g. parallelize database queries. > > > > Remember, in the wild, PHP-FPM and mod_php are by orders of magnitude the most common ways PHP is executed. React, Swoole, etc. are rounding errors in most of the market. And the alternate runtime with the most momentum is FrankenPHP, which reuses processes but is still "one request in a process at a time." > > > > Almost no one wants to spend time building code with a technology that > > isn’t supported. So when people want to do things like that, they > > simply choose another language. > > I’m not saying that async isn’t supported in CGI mode... but.. > > it’s just that a gain of a few milliseconds is unlikely to be noticeable. > > If you have a report that executes 3 queries and each query averages between 4 to 5 seconds, this report takes up to 15 seconds to run in PHP. The capability of executing async code in FPM would mean a 3x performance gain on a report like this. That is far from just a few milliseconds gain. And to be honest, the biggest gain for me would be the ability to keep the applications contextual logic within a single execution unit. One very common route that is taken with today's options is to break those 3 queries into separate HTTP endpoints and let the frontend stitch them together which provides a very similar performance gain by taking advantage of JS/Browser parallel requests since PHP is unable to do so. > -- > > > > > > > > > Marco Deleu
I'd like to mention that running queries in parallel can give better performance, but it depends on the resources available on the database server. In case disk IO is the bottleneck and a single database server is used, parallel execution can be even slower in worst case. For MySQL/MariaDB, parallel execution normally helps (see https://dev.mysql.com/doc/refman/8.0/en/faqs-general.html#faq-mysql-support-multi-core). For modern analytical databases using by default multiple CPU cores per query, column stores, simd and many other optimizations, parallelization is mostly not necessary since the connection time for a new connection is often slower than executing a query. Regards Thomas

Edmond Dantes

330 days ago
Hi.
>> But for the critical parts of a system that requires optimization of the execution duration
If you want to improve performance, you need to optimize SQL queries, not try to execute them in parallel. This can bring down the entire database (like it did today :) ) There are only a few patterns where multiple asynchronous queries can actually be useful. Hedged Requests for example. Question: how often have you seen this pattern in PHP FPM applications? Probably never :) I know it. Right now, there are only two significant PHP frameworks that are ready for stateful execution. And only one of them supports asynchronous stateful execution. This situation is caused by several reasons, and one of them is whether or not the language itself provides support for it. Why is stateful execution the primary environment for async? Because async applications are servers. And FPM is not a client-server application. It's a plugin for a server. For a very long time, PHP was essentially just a plugin for a web server. And a client-server application differs from a plugin in that it starts up and processes data streams while staying in memory. Such a process has far more use cases for async than a process that is born and dies immediately. This is the distinction I’m referring to. As for the issue with frameworks: a project with several tens of thousands of lines of code was adapted for Swoole in 2–3 weeks. It didn’t work perfectly, sometimes it would hang, but to say that it was really difficult… no, it wasn’t. Yes, there is a problem, yes, there are global states in places. But if the code was written with at least some respect for SOLID principles, this can be solved using the Context pattern. And in reality, there isn’t that much work involved, provided the abstractions were written reasonably well.
> If you have a report that executes 3 queries and each query averages between 4 to 5 seconds,
If an SQL query takes 3...5 seconds to execute, just find another developer :) Developers of network applications (I’m not talking about PHP) have accumulated a lot of optimization experience over many years of trial and error — everything has long been known. Swoole, for example, has a huge amount of experience, having essentially made the classic R/W worker architecture a standard in its ecosystem. Of course, you might say that there are simple websites for which FPM is sufficient. But over the last two years, even for simple sites, there’s TypeScript — and although its ecosystem may be weaker, the language may be more complex for some people, and its performance slightly worse — it comes with async, WebSockets, and a single language for both frontend and backend out of the box (a killer feature). And this trend is only going to grow stronger. Commercial development of mid-sized projects is the only niche that cannot be lost. These guys need Event-Driven architecture, telemetry, services. And they ask the question: why choose a language that doesn’t support modern technologies. Async is needed specifically for those technologies, not for FPM.

Larry Garfield

330 days ago
On Mon, Oct 6, 2025, at 1:50 PM, Edmond Dantes wrote:
> Of course, you might say that there are simple websites for which FPM > is sufficient. But over the last two years, even for simple sites, > there’s TypeScript — and although its ecosystem may be weaker, the > language may be more complex for some people, and its performance > slightly worse — it comes with async, WebSockets, and a single > language for both frontend and backend out of the box (a killer > feature). And this trend is only going to grow stronger. > > Commercial development of mid-sized projects is the only niche that > cannot be lost. These guys need Event-Driven architecture, telemetry, > services. And they ask the question: why choose a language that > doesn’t support modern technologies. Async is needed specifically for > those technologies, not for FPM.
We must have a different definition of mid-sized, because FPM has been used for numerous mission critical large sites, like government and university sites, and has been fine. And such sites still benefit from faster telemetry, logging, etc. Regardless, we can quibble about the percentages and what people "should" do; those are all subjective debates. The core point is this: Any async approach in core needs to treat the FPM use case as a first-class citizen, which works the same way, just as reliably, as it would in a persistent CLI command. That is not negotiable. If for no other reason than avoiding splitting the ecosystem into async/CLI and sync/FPM libraries, which would be an absolute disaster. --Larry Garfield

Rowan Tommins [IMSoP]

329 days ago
On 06/10/2025 20:18, Larry Garfield wrote:
> The core point is this: Any async approach in core needs to treat the FPM use case as a first-class citizen, which works the same way, just as reliably, as it would in a persistent CLI command. That is not negotiable. > > If for no other reason than avoiding splitting the ecosystem into async/CLI and sync/FPM libraries, which would be an absolute disaster.
I 100% agree. In fact, perhaps the single biggest benefit of having a core async model would be to reverse the current fragmentation of run-times and libraries. On 06/10/2025 19:50, Edmond Dantes wrote:
> If you want to improve performance, you need to optimize SQL queries, > not try to execute them in parallel. This can bring down the entire > database (like it did today 🙂 )
You talk as though "the database" is a single resource, which can't be scaled out. That's not the case if you have a scalable cluster of SQL/relational databases, or a dynamically sharded NoSQL/document-based data store, or are combining data from unconnected sources.
> a project with several tens of > thousands of lines of code was adapted for Swoole in 2–3 weeks. It > didn’t work perfectly, sometimes it would hang ...
2-3 weeks of development to get to something that's not even production ready is a significant investment. If your application's performance is bottlenecked on external I/O (e.g. data stores, API access), the immediate gain is probably not worth it. For those applications, the only justification for that investment is that it unlocks a further round of development to use asynchronous I/O on those bottlenecks. What would excite me is if we can get extensions and libraries to a point where we can skip the first part, and just add async I/O to a shared-nothing application.
-- Rowan Tommins [IMSoP]

=?utf-8?B?6Z+p5aSp5bOw?=

326 days ago
In my opinion,&nbsp;PHP must add asynchronous and concurrent support as soon as possible, and asynchronous IO must be regarded as a first-class citizen. Over the past few decades, the one-process-one-request model of PHP-FPM has been remarkably successful; it is simple and reliable. However, modern web applications do much more than merely reading from databases or caches, or handling internal HTTP requests—frequent cross-domain requests have become the norm. The response times for these external HTTP calls are often unpredictable. Under the PHP-FPM model, delays or timeouts from certain external APIs can easily trigger a cascading failure, bringing down the entire system. Since the emergence of ChatGPT in 2024, many software systems have been trying to integrate AI models from OpenAI, Anthropic, Google Gemini, and others. These APIs often take tens of seconds to respond,&nbsp; and PHP-FPM with multi-process is almost unavailable in such scenarios. Only asynchronous I/O offers a real solution to these challenges. Wordpress, as the PHP application with the largest number of users, may need to add LLM capabilities in the future. If PHP cannot provide support, Wordpress developers may also consider abandoning PHP and using other programming languages that support asynchronous IO for refactoring. PHP must set aside its past achievements and fully embrace the Async IO tech stack. Tianfeng Han 10/10/2025 &nbsp; &nbsp; ------------------&nbsp;Original&nbsp;------------------ From: &nbsp;"Rowan&nbsp;Tommins&nbsp;[IMSoP]"<imsop.php@rwec.co.uk&gt;; Date: &nbsp;Tue, Oct 7, 2025 05:26 AM To: &nbsp;"php internals"<internals@lists.php.net&gt;; Subject: &nbsp;Re: [PHP-DEV] PHP True Async RFC Stage 4 &nbsp; On 06/10/2025 20:18, Larry Garfield wrote: &gt; The core point is this: Any async approach in core needs to treat the FPM use case as a first-class citizen, which works the same way, just as reliably, as it would in a persistent CLI command.&nbsp; That is not negotiable. &gt; &gt; If for no other reason than avoiding splitting the ecosystem into async/CLI and sync/FPM libraries, which would be an absolute disaster. I 100% agree. In fact, perhaps the single biggest benefit of having a core async model would be to reverse the current fragmentation of run-times and libraries. On 06/10/2025 19:50, Edmond Dantes wrote: &gt; If you want to improve performance, you need to optimize SQL queries, &gt; not try to execute them in parallel. This can bring down the entire &gt; database (like it did today 🙂 ) You talk as though "the database" is a single resource, which can't be scaled out. That's not the case if you have a scalable cluster of SQL/relational databases, or a dynamically sharded NoSQL/document-based data store, or are combining data from unconnected sources. &gt; a project with several tens of &gt; thousands of lines of code was adapted for Swoole in 2–3 weeks. It &gt; didn’t work perfectly, sometimes it would hang ... 2-3 weeks of development to get to something that's not even production ready is a significant investment. If your application's performance is bottlenecked on external I/O (e.g. data stores, API access), the immediate gain is probably not worth it. For those applications, the only justification for that investment is that it unlocks a further round of development to use asynchronous I/O on those bottlenecks. What would excite me is if we can get extensions and libraries to a point where we can skip the first part, and just add async I/O to a shared-nothing application.
-- Rowan Tommins [IMSoP]

Edmond Dantes

314 days ago
Hi,
> I 100% agree. In fact, perhaps the single biggest benefit of having a > core async model would be to reverse the current fragmentation of > run-times and libraries.
This is PHP FPM + TrueAsync in Docker. You can try it. It runs with a single command, the build takes a bit of time, but that’s because it compiles from C. https://github.com/true-async/fpm It’s not that I had any doubts that async would work with FPM, but it was still necessary to verify it. I would, of course, also like to try things like PHP Native or something similar. But in principle, there shouldn’t be any difference in where or how PHP is run, because the SAPI itself doesn’t change from the perspective of the external consumer. Thanks, Ed.

Derick Rethans

323 days ago
On Mon, 6 Oct 2025, Larry Garfield wrote:
> On Mon, Oct 6, 2025, at 1:50 PM, Edmond Dantes wrote: > > > Of course, you might say that there are simple websites for which FPM > > is sufficient. But over the last two years, even for simple sites, > > there’s TypeScript — and although its ecosystem may be weaker, the > > language may be more complex for some people, and its performance > > slightly worse — it comes with async, WebSockets, and a single > > language for both frontend and backend out of the box (a killer > > feature). And this trend is only going to grow stronger. > > > > Commercial development of mid-sized projects is the only niche that > > cannot be lost. These guys need Event-Driven architecture, telemetry, > > services. And they ask the question: why choose a language that > > doesn’t support modern technologies. Async is needed specifically for > > those technologies, not for FPM. > > We must have a different definition of mid-sized, because FPM has been > used for numerous mission critical large sites, like government and > university sites, and has been fine. And such sites still benefit > from faster telemetry, logging, etc. > > Regardless, we can quibble about the percentages and what people > "should" do; those are all subjective debates. > > The core point is this: Any async approach in core needs to treat the > FPM use case as a first-class citizen, which works the same way, just > as reliably, as it would in a persistent CLI command. That is not > negotiable. > > If for no other reason than avoiding splitting the ecosystem into > async/CLI and sync/FPM libraries, which would be an absolute disaster.
I also agree with this. I tried reading the RFC today, but I ran out of time. It is *59* page printed (I didn't). I think we need to be very careful that we do not introduce a feature that allows our users to run into all sorts of problems. The symantics of such a complex feature are going to be really important. Especially about reasoning in which direction the code runs and flows, and how errors are treated. I recently read https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ which seems like an entirely sensible way of proceeding. Although the title talks about Go and its problems, the "Structured Concurrency" approach is more of a way of doing concurrency right, without the possibility of our users getting into trouble. I don't think the RFC as-is is close to this at all — but I have mostly skimmed it so far. I would also believe that discussion how this should work would work better with a group of people - preferably in real-time - and not as an idea and implementation of a single person. I know others have been reviewing and commenting on it, but I don't think that's quite the same. Concurrency in all its forms is a complex subject, and we can't really get this wrong as we'll have to live with the concepts for a long time. cheers, Derick
-- https://derickrethans.nl | https://xdebug.org | https://dram.io Author of Xdebug. Like it? Consider supporting me: https://xdebug.org/support mastodon: @derickr@phpc.social @xdebug@phpc.social

Edmond Dantes

322 days ago
Hello.
> I tried reading the RFC today, but I ran out of time. It is *59* page printed (I didn't).
...
> I don't think the RFC as-is is close to this at all — but I have mostly skimmed it so far.
**Thank you for the feedback.** This time there will be a vote. If this RFC is not accepted, I promise that I will not create a fifth version. So if anyone has something to say, please feel free to speak openly. Please.

Rob Landers

321 days ago
On Sun, Oct 5, 2025, at 07:23, Edmond Dantes wrote:
> Good day, everyone. I hope you're doing well. > > I’m happy to present the fourth version of the RFC. It wasn’t just me > who worked on it — members of the PHP community contributed as well. > Many thanks to everyone for your input! > > https://wiki.php.net/rfc/true_async > > **What has changed in this version?** > > The RFC has been significantly simplified: > > 1. Components (such as TaskGroup) that can be discussed in separate > RFCs have been removed from the current one. > 2. Coroutines can now be created anywhere — even inside shutdown_function. > 3. Added Memory Management and Garbage Collection section > > Although work on the previous API RFC was interrupted and we weren’t > able to include it in PHP 8.5, it still provided valuable feedback on > the Async API code. > > During this time, I managed to refactor and optimize the TrueAsync > code, which showed promising performance results in I/O scenarios. > > A test integration between **NGINX UNIT** and the **TrueAsync API** > was implemented to evaluate the possibility of using PHP as an > asynchronous backend for a web server: > https://github.com/EdmondDantes/nginx-unit/tree/true-async/src/true-async-php > > During this time, the project has come very close to beta status. > > Once again, I want to thank everyone who supported me during difficult > times, offered advice, and helped develop this project. > > Given the maturity of both the code and the RFC, this time I hope to > proceed with a vote. > > Wishing you all a great day, and thank you for your feedback! >
Hello, I’m not even half way done with a list of comments and questions, but I have one that continues to bother me while reading, so I figure I will just ask it. Why doesn’t scope implement Awaitable? — Rob

Edmond Dantes

321 days ago
Hello.
> Why doesn’t scope implement Awaitable?
Let’s say there’s a programmer named *John* who is writing a library, and he has a function that calls an external handler. Programmer *Robert* wrote the `externalHandler`. ```php function processData(string $url, callable $externalHandler): void { ... $externalHandler($result); } ``` John knows which contracts are inside processData, but he knows nothing about `$externalHandler`. From the perspective of `processData`, `$externalHandler` acts as a **black box**. If John uses `await()` on that black box, it may lead to an infinite wait. There are two solutions to this problem: 1. Delegate full responsibility for the program’s behavior to *Robert*, meaning to `$externalHandler` 2. Establish a limiting contract Therefore, if a `Scope` needs to be awaited, it can only be done together with a cancellation token. In real-world scenarios, awaiting a `Scope` during normal execution makes no sense, because you have a `cancellation policy`. This means that at any necessary moment you can dispose() the `Scope` and thus interrupt the execution of tasks inside the black box. For the `TaskGroup` pattern, which exists in the third version of the RFC, awaiting is a relatively safe operation, because in this case we assume that the code is written by someone who has direct access to the agreements and bears full responsibility for any errors. So, `Scope` is intended for components where responsibility is shared between different programmers, while `TaskGroup` should be used when working with a clearly defined set of coroutines.

Rob Landers

320 days ago
On Wed, Oct 15, 2025, at 09:53, Edmond Dantes wrote:
> Hello. > > > Why doesn’t scope implement Awaitable? > > Let’s say there’s a programmer named *John* who is writing a library, > and he has a function that calls an external handler. > Programmer *Robert* wrote the `externalHandler`. > > ```php > function processData(string $url, callable $externalHandler): void > { > ... > $externalHandler($result); > } > ``` > > John knows which contracts are inside processData, but he knows > nothing about `$externalHandler`. > From the perspective of `processData`, `$externalHandler` acts as a > **black box**. > If John uses `await()` on that black box, it may lead to an infinite wait. > > There are two solutions to this problem: > > 1. Delegate full responsibility for the program’s behavior to > *Robert*, meaning to `$externalHandler` > 2. Establish a limiting contract > > Therefore, if a `Scope` needs to be awaited, it can only be done > together with a cancellation token. > > In real-world scenarios, awaiting a `Scope` during normal execution > makes no sense, because you have a `cancellation policy`. > This means that at any necessary moment you can dispose() the `Scope` > and thus interrupt the execution of tasks inside the black box. > > For the `TaskGroup` pattern, which exists in the third version of the > RFC, awaiting is a relatively safe operation, because in this case we > assume that the code is written by someone who has direct access to > the agreements and bears full responsibility for any errors. > > So, `Scope` is intended for components where responsibility is shared > between different programmers, while `TaskGroup` should be used when > working with a clearly defined set of coroutines. >
I don’t get it. What does different programmers working on a program have to do with whether or not scopes implements Awaitable? Scope has an await method, it should be Awaitable. await() takes a cancellation and thus anything Awaitable can be cancelled at any time. I don’t see why scope is special in that regard.
> If John uses `await()` on that black box, it may lead to an infinite wait.
This is true of any software or code. Knowing whether or not something will ever complete is called The Halting Problem. It is unsolvable, in the general sense. You can await() a read of an infinite file, or a remote file that will take 5y to read because it is being read at 1 bps. Your clock can fry on your motherboard, preventing timeouts from ever firing. Your disk can die mid-read, preventing it from ever sending you any data. There is so much that can go wrong. To say that something that has an await method isn’t Awaitable because it may never return is true for ALL Awaitable tasks as well. It isn’t special. — Rob

Edmond Dantes

320 days ago
> I don’t get it. What does different programmers working
My main point was about contracts. Developers were used to demonstrate breaches of agreements. A properly defined contract with a black box helps identify errors and limit their impact. I don’t know how to explain it more simply. These are fundamental elements of design in IT.

Rob Landers

320 days ago
On Thu, Oct 16, 2025, at 08:24, Edmond Dantes wrote:
> > I don’t get it. What does different programmers working > My main point was about contracts. > Developers were used to demonstrate breaches of agreements. > A properly defined contract with a black box helps identify errors and > limit their impact. > I don’t know how to explain it more simply. These are fundamental > elements of design in IT.
You've provided examples and said that it violates design and fundamental elements, but not which design and fundamentals, ie, the evidence for the decision. I would expect more than "because I said so" for such a huge language feature, but rather arguments grounded in computer science. People are going to ask this question, it will probably be in the docs, so, there needs to be a good answer. — Rob

Edmond Dantes

320 days ago
> I would expect more than "because I said so"
In my response I provided arguments, but they were ignored. If you tell me exactly what is unclear to you, I can give a concrete, well-reasoned answer.

Rob Landers

320 days ago
On Thu, Oct 16, 2025, at 09:08, Edmond Dantes wrote:
> > I would expect more than "because I said so" > > In my response I provided arguments, but they were ignored. If you > tell me exactly what is unclear to you, I can give a concrete, > well-reasoned answer. >
This is all I have to go off of, and my explicit rebuttals as to why they are not reasons:
> Therefore, if a `Scope` needs to be awaited, it can only be done > together with a cancellation token.
This is effectively not true. I can pass a cancellation token that never cancels. Sometimes, this is exactly the behaviour that is desired.
> In real-world scenarios, awaiting a `Scope` during normal execution > makes no sense, because you have a `cancellation policy`. > This means that at any necessary moment you can dispose() the `Scope` > and thus interrupt the execution of tasks inside the black box.
That sounds like a feature, not a reason.
> For the `TaskGroup` pattern, which exists in the third version of the > RFC, awaiting is a relatively safe operation, because in this case we > assume that the code is written by someone who has direct access to > the agreements and bears full responsibility for any errors.
We aren't talking about TaskGroups, but regardless, I fail to understand how that makes scopes 'dangerous' to await and why it should not be Awaitable.
> So, `Scope` is intended for components where responsibility is shared > between different programmers, while `TaskGroup` should be used when > working with a clearly defined set of coroutines.
My question is "why does this mean something with an await method isn't Awaitable?" -- it seems that this is orthogonal to the interface and the reason why it should/should not be Awaitable. — Rob

Edmond Dantes

320 days ago
The quotes you provided were not my arguments. 1. There is an `await()` function. This function has a cancellation token that allows limiting its lifetime. 2. There is a `Scope`. It is undesirable to wait on the Scope without an explicit limit, because while waiting, a reference to it is held, and if the tasks within the Scope contain an error, the application ends up in an undefined state. 3. However, if the developer explicitly defines a contract that limits the lifetime using a cancellation token, such an operation becomes safe. 4. But in await(), the cancellation token is an optional parameter. Is that clear?

Edmond Dantes

320 days ago
> That sounds like a feature, not a reason.
There is almost nothing in this RFC or perhaps nothing at all that exists by accident. Every class, function, every policy was created to solve real problems. This document is the result of long-term analysis and studying cases, not just a “because I felt like it” approach.

Daniil Gentili

320 days ago
> You've provided examples and said that it violates design and fundamental
elements, but not which design and fundamentals, ie, the evidence for the decision. I would expect more than "because I said so" for such a huge language feature, but rather arguments grounded in computer science. He very explicitly described the issue, in objective terms: a breach of agreement in the context of the invocation of a foreign interface. In simpler terms, you can't and should not be able to mess with the internals of code you didn't write: this is similar to the principle of encapsulation in OOP, where you cannot modify private properties of an object. Cancellation should be part of the contract of an async function: it is safe, and most async languages already implement it explicitly by passing a context or a cancellation token, the current approach of the RFC does it implicitly, which is also fine. Awaiting for the completion of spawned coroutines should *not* be part of the contract of an async function: it is an incredibly easy footgun, as it's incredibly easy to spawn a coroutine meant to i.e. run until the object is destroyed, and then encounter a deadlock when the linked scope is awaited for before the object is destroyed (even messier if cycles and thus the GC are involved). Languages like Kotlin that do implement await on scopes have already realized that it is a mistake, as can be seen by the many warnings against using await on a scope, as I already linked in previous emails. On the other hand, making the same mistake described above, by cancelling a scope, will produce a very easy to debug exception instead of a deadlock, easily fixable by (warning the author of the library class) to use a new scope to spawn coroutines within the object. Awaiting a scope leads to deadlocks in case where a separate scope is needed but not used, cancelling them leads to a simple exception. Awaiting on multiple tasks can already be done, explicitly, with TaskGroup. Regards, Daniil Gentili.

Rob Landers

320 days ago
On Thu, Oct 16, 2025, at 09:38, Daniil Gentili wrote:
> > > You've provided examples and said that it violates design and fundamental elements, but not which design and fundamentals, ie, the evidence for the decision. I would expect more than "because I said so" for such a huge language feature, but rather arguments grounded in computer science. > > > He very explicitly described the issue, in objective terms: a breach of agreement in the context of the invocation of a foreign interface. > > In simpler terms, you can't and should not be able to mess with the internals of code you didn't write: this is similar to the principle of encapsulation in OOP, where you cannot modify private properties of an object. > > > Cancellation should be part of the contract of an async function: it is safe, and most async languages already implement it explicitly by passing a context or a cancellation token, the current approach of the RFC does it implicitly, which is also fine. > > Awaiting for the completion of spawned coroutines should *not* be part of the contract of an async function: it is an incredibly easy footgun, as it's incredibly easy to spawn a coroutine meant to i.e. run until the object is destroyed, and then encounter a deadlock when the linked scope is awaited for before the object is destroyed (even messier if cycles and thus the GC are involved). > > Languages like Kotlin that do implement await on scopes have already realized that it is a mistake, as can be seen by the many warnings against using await on a scope, as I already linked in previous emails. > > On the other hand, making the same mistake described above, by cancelling a scope, will produce a very easy to debug exception instead of a deadlock, easily fixable by (warning the author of the library class) to use a new scope to spawn coroutines within the object. > > Awaiting a scope leads to deadlocks in case where a separate scope is needed but not used, cancelling them leads to a simple exception. > Awaiting on multiple tasks can already be done, explicitly, with TaskGroup. > > > Regards, > Daniil Gentili.
Hey Daniil and Edmond, I think I understand the intention. Would it be better to instead of having ->awaitCompletion (which feels like an implicit implementation of Awaitable without an explicit implmentation -- which is the part that was bothering me), maybe having something like ->joinAll() or ->joinOnCancellation()? That way someone like me won't come along and wrap them in an Awaitable because it looks Awaitable. It also might be a good idea to make specifying a timeout in ms mandatory, instead of a taking an Awaitable/Cancellation. This would also prevent people from simply passing a "never returning" Awaitable thinking they're being clever. It also might be good to provide a realistic looking example showing a "bad case" of how this is dangerous instead of simply saying that it is, showing how a scope is not a 'future', but a container, and preemptively mention TaskGroups, linking to the future scope (which it should also probably be listed there as well). — Rob

Edmond Dantes

320 days ago
> Would it be better to instead of having ->awaitCompletion (which feels like an implicit implementation of Awaitable without an explicit implmentation -- which is the part that was bothering me), maybe having something like ->joinAll() or ->joinOnCancellation()? > That way someone like me won't come along and wrap them in an Awaitable because it looks Awaitable.
For example, you have an interface `DataBaseAdmin` with a `removeDB()` method and a class that implements it. You need to be able to delete the database, but with an additional condition. So you create a decorator class with a method `ContextualDBAdmin::removeDBWhen($extraRules)`. As a result, the decorator class is logically associated with the `DataBaseAdmin` interface. The question is: **so what?**
> It also might be a good idea to make specifying a timeout in ms mandatory, instead of a taking an Awaitable/Cancellation.
The idea is correct, but there will definitely be someone who says they need more flexibility. And it's true you can create a `DeferredCancellation` and forget to finish it. :) There are a lot of such subtle points, and they can be discussed endlessly. But I wouldn’t spend time on them.
> It also might be good to provide a realistic looking example showing a "bad case" of how this is dangerous instead of simply saying that it is, showing how a scope is not a 'future', > but a container, and preemptively mention TaskGroups, linking to the future scope (which it should also probably be listed there as well).
1. It’s very difficult to write a realistic example that’s still small. 2. The `TaskGroup` or `CoroutineGroup` class is left for future discussion. In the final documentation, it will be exactly as you suggested.

Rob Landers

320 days ago
On Thu, Oct 16, 2025, at 15:19, Edmond Dantes wrote:
> > Would it be better to instead of having ->awaitCompletion (which feels like an implicit implementation of Awaitable without an explicit implmentation -- which is the part that was bothering me), maybe having something like ->joinAll() or ->joinOnCancellation()? > > That way someone like me won't come along and wrap them in an Awaitable because it looks Awaitable. > > For example, you have an interface `DataBaseAdmin` with a `removeDB()` > method and a class that implements it. > You need to be able to delete the database, but with an additional > condition. So you create a decorator class with a method > `ContextualDBAdmin::removeDBWhen($extraRules)`. > > As a result, the decorator class is logically associated with the > `DataBaseAdmin` interface. > > The question is: **so what?**
I think we might be talking past each other a little. You said earlier that one of the goals here is to prevent misuse (e.g. unbounded or foreign awaits). I completely agree and that’s exactly why I’m suggesting a rename. From the outside, a method called awaitCompletion() looks and behaves like an Awaitable, even though the type explicitly isn’t one. That contradiction encourages people to wrap it to “make it awaitable,” which re-introduces the very problem you’re trying to avoid. Renaming it to something like joinAll() or joinAfterCancellation() keeps the semantics intact while communicating the intent: this isn’t a future; it’s a container join. That small change would make the interface self-documenting and harder to misuse.
> > > It also might be a good idea to make specifying a timeout in ms mandatory, instead of a taking an Awaitable/Cancellation. > > The idea is correct, but there will definitely be someone who says > they need more flexibility. > And it's true you can create a `DeferredCancellation` and forget to > finish it. :) > > There are a lot of such subtle points, and they can be discussed endlessly. > But I wouldn’t spend time on them.
That's what we are here to do, no? Discussion is useful if it makes things better than the sum of their parts.
> > It also might be good to provide a realistic looking example showing a "bad case" of how this is dangerous instead of simply saying that it is, showing how a scope is not a 'future', > > but a container, and preemptively mention TaskGroups, linking to the future scope (which it should also probably be listed there as well). > > 1. It’s very difficult to write a realistic example that’s still small.
I'll give it a go: $scope = new Scope(); // Library code spawns in my scope (transitively) $scope->spawn(fn() => thirdPartyOperation()); // may spawn more // Looks innocent, but this can wait on foreign work: $scope->awaitCompletion(Async\timeout(60000)); // rename -> joinAll(...)?
> 2. The `TaskGroup` or `CoroutineGroup` class is left for future > discussion. In the final documentation, it will be exactly as you > suggested.
My point is that it wasn't listed in "future scope" of the RFC, though they're mentioned throughout the document, in passing. — Rob

Edmond Dantes

317 days ago
Hello everyone, Tomorrow marks two weeks since the RFC was published, which means that, formally, it is now eligible to be submitted for voting. Allow me to briefly summarize the current state of the project: 1. The implementation has been delivered both as **core changes to PHP** and as a **separate extension**. 2. **More than 50 PHP functions** have been adapted to work in **non-blocking mode**, all of which are fully **covered by tests**. 3. There is a working **integration example with a web server** using the model _“react-process + embedded PHP process”_. 4. The project is currently in an **Alpha+ stage**, and is **available for testing and experimentation**. 5. The **RFC v3 has been implemented almost entirely**, and **RFC v4 is fully covered**. 6. The **second version of the PHP Stream integration** demonstrates **strong I/O performance** in **web server scenarios**. 7. The code introduces asynchrony into all stages of PHP execution, including ensuring correct GC operation. It successfully passes the standard PHP test suite. 8. Supports both ZTS and non-ZTS builds. 9. Separate testing was performed with XDebug (with a small patch applied). At this stage, all technical objectives of the project have been achieved 100%. To emphasize: the project is not yet production-ready, but it has fully achieved its demonstration goals. Around this time, the project turned one year old. **RFC** 1. A high-level API for PHP land has been developed, based on the experience of other programming languages. 2. Known development pitfalls have been taken into account, and fault-tolerant solutions have been incorporated. At this point, all RFC objectives have been achieved. My special thanks go to Roman Pronskiy, Jakub Zelenka, Arnaud Le Blanc, Valentin Udaltsov as well as to everyone who supported the project in various ways. Dear PHP community, we are now at a decision point: what should be done next? I am obliged to draw the attention of the PHP community to the fact that the Swow project has existed for several years. Unlike TrueAsync, it is more **mature**, and its author Twose has previously expressed an intention to integrate it into the core. This RFC can be used as a foundation regardless of the implementation. So I believe that Twose’s opinion is important and should be taken into consideration. I recommend voting for the TrueAsync RFC with the status of “experimental”. Although this status is not formally defined in PHP rules, it would allow framework authors, library developers, and other maintainers to treat this RFC and its implementation as something expected to be adopted in the future. Without this step, further development makes little sense. There is no point in creating an equivalent of Swoole or Swow. These projects already exist, are excellently built, and fulfill their purpose. **For the moderators** It is possible that voting on this RFC may not make sense. If that is the case, please state so within the next 2-3 days. This would be a rational and respectful approach toward the participants of the vote. Thank you all, and best of luck.

Alexandru Pătrănescu

317 days ago
Hi, On Sun, Oct 19, 2025 at 8:06 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Hello everyone, > > Tomorrow marks two weeks since the RFC was published, which means > that, formally, it is now eligible to be submitted for voting. > > >
I just want to say that the amount of effort put into this is really impressive. So, thank you! At the same time, the reviewing effort is also big, and I can hope you will keep this in mind. There is no need to go with the minimum period of two weeks. I would very much want to see this proposal succeed. I think you should allow for a lengthier review period so that people will have enough time to contribute their thoughts and ideas, to make sure all unclarities are cleared up, and so, to have a better chance of being accepted. Thank you again, Alex

Edmond Dantes

317 days ago
Hello.
> I think you should allow for a lengthier review period so that people will have enough time to contribute their thoughts and ideas, > to make sure all unclarities are cleared up, and so, to have a better chance of being accepted.
I’m happy to allow as much time as needed. How about we extend the review period by another two weeks? Thank you, Ed

Rob Landers

316 days ago
On Sun, Oct 19, 2025, at 12:41, Edmond Dantes wrote:
> Hello. > > > I think you should allow for a lengthier review period so that people will have enough time to contribute their thoughts and ideas, > > to make sure all unclarities are cleared up, and so, to have a better chance of being accepted. > > I’m happy to allow as much time as needed. How about we extend the > review period by another two weeks? > > Thank you, Ed >
That would be great, As mentioned in my last email, I was only half way through (now closer to 60%) of making notes and reviewing the proposal. This isn't a simple proposal but rather a complex one with interleaving behaviours. It takes awhile to digest and understand, plus run through various scenarios and understand how it will work. — Rob

Rob Landers

315 days ago
On Sun, Oct 5, 2025, at 07:23, Edmond Dantes wrote:
> Good day, everyone. I hope you're doing well. > > I’m happy to present the fourth version of the RFC. It wasn’t just me > who worked on it — members of the PHP community contributed as well. > Many thanks to everyone for your input! > > https://wiki.php.net/rfc/true_async > > **What has changed in this version?** > > The RFC has been significantly simplified: > > 1. Components (such as TaskGroup) that can be discussed in separate > RFCs have been removed from the current one. > 2. Coroutines can now be created anywhere — even inside shutdown_function. > 3. Added Memory Management and Garbage Collection section > > Although work on the previous API RFC was interrupted and we weren’t > able to include it in PHP 8.5, it still provided valuable feedback on > the Async API code. > > During this time, I managed to refactor and optimize the TrueAsync > code, which showed promising performance results in I/O scenarios. > > A test integration between **NGINX UNIT** and the **TrueAsync API** > was implemented to evaluate the possibility of using PHP as an > asynchronous backend for a web server: > https://github.com/EdmondDantes/nginx-unit/tree/true-async/src/true-async-php > > During this time, the project has come very close to beta status. > > Once again, I want to thank everyone who supported me during difficult > times, offered advice, and helped develop this project. > > Given the maturity of both the code and the RFC, this time I hope to > proceed with a vote. > > Wishing you all a great day, and thank you for your feedback! >
Hey Edmond, I'm not quite finished notating the whole thing, but let's start with this: AWAITABLE There's something here that bothers me. The RFC says:
> he `Awaitable` interface is a contract that allows objects to be used in the `await` expression. > The `Awaitable` interface does not impose limitations on the number of state changes. > In the general case, objects implementing the `Awaitable` interface can act as triggers — that is, they can change their state an unlimited number of times. This means that multiple calls to `await <Awaitable>` may produce different results.
But then coroutines say:
> *Coroutines behave like Futures:* > once a coroutine completes (successfully, with an exception, or through cancellation), > it preserves its final state. > Multiple calls to `await()` on the same coroutine will always return the same result or > throw the same exception.
This seems a bit contradictory and confuses things. When I await(), do I need to do it in a loop, or just once? It might be a good idea to make a couple subtypes: Signal and Future. Coroutines become Future that only await once, while Signal is something that can be awaited many times. It probably won't change much from the C point of view, but it would change how we implement them in general libraries: if ($awaitable instanceof Trigger) { // throw or maybe loop over the trigger value? } CANCELLATIONS The RFC says:
> In the context of coroutines, it is not recommended to use `catch \Throwable` or `catch CancellationError`.
But the example setChildScopeExceptionHandler does exactly this! Further, much framework/app code uses the $previous to wrap exceptions as they bubble up, so it might be nice to have an Async\isCancellation(Throwable): bool function that can efficiently walk the exception chain and tell us if any cancellation was involved. Minor nit: in the Async\protect section, it would be nice to say that cancellations being AFTER the protect() are guaranteed, and also specify reentry/nesting of protect(). Like what happens here: Async\protect(foo(...)); // foo also calls protect() And for reentrancy, foo() -> bar() -> foo() -> bar() and foo() calls protect(). Which one gets the cancellation? I also think that calling it a "critical section" is a misnomer, as that traditionally indicates a "lock" (only one thread can execute that section at a time) and not "this can't be cancelled". Also, if I'm reading this correctly, a coroutine can mark itself as canceled, yet run to completion; however anyone await()'ing it, will get a CancellationException instead of the completed value? DESTRUCTORS Allowing destructors to spawn feels extremely dangerous to me (but powerful). These typically -- but not always -- run between the return statement and the next line (typically best to visualize that as the "}" since it runs in the original scope IIRC). That could make it 'feel like' methods/functions are hanging or never returning if a library abuses this by suspending or awaiting something. ZOMBIES async.zombie_coroutine_timeout says 2 seconds in the text, but 5 seconds in the php.ini section. The RFC says:
> Once the application is considered finished, zombie coroutines are given a time limit within which they must complete execution. If this limit is exceeded, all zombie coroutines are canceled.
What is defined as "application considered finished?" FrankenPHP workers, for instance, don’t "finish" — is there a way to reap zombies manually? Then there is dispose() and disposeSafely(), it would be good to specify ordering and finally/onFinally execution here. ie, in nested scopes, does it go from inner -> outer scopes, in the order they are created? When does finally/onFinally execute in that context? FIBERS Fibers are proliferant in existing code. It would be a good idea to provide a few helpers to allow code to migrate. Maybe something like Async\isEnabled() to know whether I should use fibers or not. Nit: the error message has a grammatical error: "Cannot create a fiber while **an** True Async is active" should be "Cannot create a fiber while True Async is active"? SHUTDOWN Is there also a timeout on Phase 1 shutdown? Otherwise, if it is only an exception, then this could hang forever. EXCEPTION IDENTITY The RFC says:
> Multiple calls to `await()` on the same coroutine will always return the same result or > throw the same exception.
Is this "same exception" mean this literally, or is it a clone? If it is the same, what prevents another code path from mutating the original exception before it gets to me? TYPOS - fix `file_get_content` to `file_get_contents` in examples. - I think get_last_error() should be error_get_last()? - you call "suspend" a keyword in several places but it is actually a function. - examples use sleep(), but don't clarify whether sleep() will be blocking or non-blocking. - Sometimes you use AwaitCancelledException and other times CancellationError. Which is it? — Rob

Rob Landers

315 days ago
On Tue, Oct 21, 2025, at 08:46, Rob Landers wrote:
> On Sun, Oct 5, 2025, at 07:23, Edmond Dantes wrote: >> Good day, everyone. I hope you're doing well. >> >> I’m happy to present the fourth version of the RFC. It wasn’t just me >> who worked on it — members of the PHP community contributed as well. >> Many thanks to everyone for your input! >> >> https://wiki.php.net/rfc/true_async >> >> **What has changed in this version?** >> >> The RFC has been significantly simplified: >> >> 1. Components (such as TaskGroup) that can be discussed in separate >> RFCs have been removed from the current one. >> 2. Coroutines can now be created anywhere — even inside shutdown_function. >> 3. Added Memory Management and Garbage Collection section >> >> Although work on the previous API RFC was interrupted and we weren’t >> able to include it in PHP 8.5, it still provided valuable feedback on >> the Async API code. >> >> During this time, I managed to refactor and optimize the TrueAsync >> code, which showed promising performance results in I/O scenarios. >> >> A test integration between **NGINX UNIT** and the **TrueAsync API** >> was implemented to evaluate the possibility of using PHP as an >> asynchronous backend for a web server: >> https://github.com/EdmondDantes/nginx-unit/tree/true-async/src/true-async-php >> >> During this time, the project has come very close to beta status. >> >> Once again, I want to thank everyone who supported me during difficult >> times, offered advice, and helped develop this project. >> >> Given the maturity of both the code and the RFC, this time I hope to >> proceed with a vote. >> >> Wishing you all a great day, and thank you for your feedback! >> > > Hey Edmond, > > I'm not quite finished notating the whole thing, but let's start with this: > > AWAITABLE > > There's something here that bothers me. The RFC says: > >> he `Awaitable` interface is a contract that allows objects to be used in the `await` expression. >> The `Awaitable` interface does not impose limitations on the number of state changes. >> In the general case, objects implementing the `Awaitable` interface can act as triggers — that is, they can change their state an unlimited number of times. This means that multiple calls to `await <Awaitable>` may produce different results. > > But then coroutines say: > >> *Coroutines behave like Futures:* >> once a coroutine completes (successfully, with an exception, or through cancellation), >> it preserves its final state. >> Multiple calls to `await()` on the same coroutine will always return the same result or >> throw the same exception. > > This seems a bit contradictory and confuses things. When I await(), do I need to do it in a loop, or just once? It might be a good idea to make a couple subtypes: Signal and Future. Coroutines become Future that only await once, while Signal is something that can be awaited many times. > > It probably won't change much from the C point of view, but it would change how we implement them in general libraries: > > if ($awaitable instanceof Trigger) { > // throw or maybe loop over the trigger value? > } > > CANCELLATIONS > > The RFC says: > >> In the context of coroutines, it is not recommended to use `catch \Throwable` or `catch CancellationError`. > > But the example setChildScopeExceptionHandler does exactly this! Further, much framework/app code uses the $previous to wrap exceptions as they bubble up, so it might be nice to have an Async\isCancellation(Throwable): bool function that can efficiently walk the exception chain and tell us if any cancellation was involved. > > Minor nit: in the Async\protect section, it would be nice to say that cancellations being AFTER the protect() are guaranteed, and also specify reentry/nesting of protect(). Like what happens here: > > Async\protect(foo(...)); // foo also calls protect() > > And for reentrancy, foo() -> bar() -> foo() -> bar() and foo() calls protect(). > > Which one gets the cancellation? I also think that calling it a "critical section" is a misnomer, as that traditionally indicates a "lock" (only one thread can execute that section at a time) and not "this can't be cancelled". > > Also, if I'm reading this correctly, a coroutine can mark itself as canceled, yet run to completion; however anyone await()'ing it, will get a CancellationException instead of the completed value? > > DESTRUCTORS > > Allowing destructors to spawn feels extremely dangerous to me (but powerful). These typically -- but not always -- run between the return statement and the next line (typically best to visualize that as the "}" since it runs in the original scope IIRC). That could make it 'feel like' methods/functions are hanging or never returning if a library abuses this by suspending or awaiting something. > > ZOMBIES > > async.zombie_coroutine_timeout says 2 seconds in the text, but 5 seconds in the php.ini section. > > The RFC says: > >> Once the application is considered finished, zombie coroutines are given a time limit within which they must complete execution. If this limit is exceeded, all zombie coroutines are canceled. > > What is defined as "application considered finished?" FrankenPHP workers, for instance, don’t "finish" — is there a way to reap zombies manually? > > Then there is dispose() and disposeSafely(), it would be good to specify ordering and finally/onFinally execution here. ie, in nested scopes, does it go from inner -> outer scopes, in the order they are created? When does finally/onFinally execute in that context? > > FIBERS > > Fibers are proliferant in existing code. It would be a good idea to provide a few helpers to allow code to migrate. Maybe something like Async\isEnabled() to know whether I should use fibers or not. > > Nit: the error message has a grammatical error: "Cannot create a fiber while **an** True Async is active" should be "Cannot create a fiber while True Async is active"? > > SHUTDOWN > > Is there also a timeout on Phase 1 shutdown? Otherwise, if it is only an exception, then this could hang forever. > > EXCEPTION IDENTITY > > The RFC says: > >> Multiple calls to `await()` on the same coroutine will always return the same result or >> throw the same exception. > > Is this "same exception" mean this literally, or is it a clone? If it is the same, what prevents another code path from mutating the original exception before it gets to me? > > TYPOS > > - fix `file_get_content` to `file_get_contents` in examples. > - I think get_last_error() should be error_get_last()? > - you call "suspend" a keyword in several places but it is actually a function. > - examples use sleep(), but don't clarify whether sleep() will be blocking or non-blocking. > - Sometimes you use AwaitCancelledException and other times CancellationError. Which is it? > > — Rob
For bike shedding purposes: It's also worth pointing out that "cancelled" is the British spelling, while "canceled" is the American spelling. PHP is already inconsistent in the docs/error messages, but I don't see any types/functions with either spelling. Generally, PHP tends to follow American spelling for the standard lib (color vs. colour, behavior vs behaviour, analyzes vs analyses, initialize vs initialise, serialize vs serialise, etc). So, IMHO, we should probably be using the american spelling here ... — Rob

Edmond Dantes

315 days ago
> When I await(), do I need to do it in a loop, or just once?
It depends on what is being awaited. On one hand, it would probably be convenient to have many different operations for different cases, but then we make the language semantics more complex.
> Coroutines become Future that only await once, while Signal is something that can be awaited many times.
At the moment, only such objects exist. It’s hard to say whether there will be others. Although one can imagine an Interval object, there are some doubts about whether such an object should be used in a while await loop, because from a performance standpoint, it’s not very efficient.
> But the example setChildScopeExceptionHandler does exactly this!
The Scope-level handler does not interfere with coroutine completion. And it is not called because the cancellation exception is "absorbed" by the coroutine.
> Further, much framework/app code uses the $previous to wrap exceptions as they bubble up,
If a programmer wants to wrap an exception in their own one let them. No one forbids catching exceptions; they just shouldn’t be suppressed.
> Async\isCancellation(Throwable): bool
Why make a separate function if you can just walk through the chain?
> Minor nit: in the Async\protect section, it would be nice to say that cancellations being AFTER the protect() are guaranteed, and also specify reentry/nesting of protect(). Like what happens here:
That’s a good case! Re-entering protect should be forbidden that must not be allowed.
> Also, if I'm reading this correctly, a coroutine can mark itself as canceled, yet run to completion; however anyone await()'ing it, will get a CancellationException instead of the completed value?
If a coroutine is canceled, its return value will be ignored. However, of course, it can still call return, and that will work without any issues. I considered issuing a warning for such behavior but later removed it, since I don’t see it as particularly dangerous. This point requires attention, because there’s a certain “flexibility” here that can be confusing. However, the risk in this case is low.
> Allowing destructors to spawn feels extremely dangerous to me (but powerful). These typically -- but not always -- run between the return statement and the next line (typically best to visualize that > as the "}" since it runs in the original scope IIRC). That could make it 'feel like' methods/functions are hanging or never returning if a library abuses this by suspending or awaiting something.
Launching coroutines in destructors is indeed a relatively dangerous operation, but for different reasons mainly related to who owns such coroutines. However, I didn’t quite understand what danger you were referring to? Asynchronous operations, as well as coroutine launching, are indeed used in practice. The code executes properly, so I don’t quite see what risks there could be, apart from potential resource leaks caused by faulty coroutines.
> async.zombie_coroutine_timeout says 2 seconds in the text, but 5 seconds in the php.ini section.
Thanks.
> What is defined as "application considered finished?" FrankenPHP workers, for instance, don’t "finish" — is there a way to reap zombies manually?
The Scheduler keeps track of the number of coroutines being executed. When the number of active coroutines reaches zero, the Scheduler stops execution. Zombie coroutines are not counted among those that keep the execution running. If PHP is running in worker mode, then the worker code must correctly keep the execution active. But even workers sometimes need to shutdown.
> it would be good to specify ordering and finally/onFinally execution here
Doesn’t the RFC define the order of onFinally handler execution? onFinally handlers are executed after the coroutine or the Scope has completed. onFinally is not directly related to dispose() in any way. When dispose() is called, coroutine cancellation begins. This process may take some time. Only after the last coroutine has stopped will onFinally be invoked. In other words, you should not attempt to link the calls of these methods in any way.
> Maybe something like Async\isEnabled() to know whether I should use fibers or not.
Good idea!, considering that such a function actually exists at the C code level.
> Is this "same exception" mean this literally, or is it a clone? If it is the same, what prevents another code path from mutating the original exception before it gets to me?
It’s the exact same object that is, a reference to the same instance. So if someone modifies it, those changes will, of course, take effect.
> Is there also a timeout on Phase 1 shutdown? Otherwise, if it is only an exception, then this could hang forever.
That’s true! A hang is indeed possible. I’m still not sure whether it’s worth adding an auxiliary mechanism to handle such cases, because that would effectively make PHP “smarter” than the programmer. I believe that a language should not try to be smarter than the programmer if the application runs in a certain way, then it’s probably meant to be that way.
> - Sometimes you use AwaitCancelledException and other times CancellationError. Which is it?
The old exception name apparently hasn’t been updated to the new one everywhere.
> Nit: the error message has a grammatical error: "Cannot create a fiber while an True Async is active" should be "Cannot create a fiber while True Async is active"? >
Thanks!

Rob Landers

315 days ago
On Tue, Oct 21, 2025, at 15:33, Edmond Dantes wrote:
> > When I await(), do I need to do it in a loop, or just once? > > It depends on what is being awaited. > On one hand, it would probably be convenient to have many different > operations for different cases, but then we make the language > semantics more complex.
> > Coroutines become Future that only await once, while Signal is something that can be awaited many times. > At the moment, only such objects exist. It’s hard to say whether there > will be others. > Although one can imagine an Interval object, there are some doubts > about whether such an object should be used in a while await loop, > because from a performance standpoint, it’s not very efficient.
It might be good to clarify this when we talk about the Awaitable Interface then? Maybe something like: "In PHP 8.6 the only awaitables are single-completion. Future versions may add multi-event awaitables." just to clear it up for early adopters?
> > But the example setChildScopeExceptionHandler does exactly this! > The Scope-level handler does not interfere with coroutine completion. > And it is not called because the cancellation exception is "absorbed" > by the coroutine.
:thumbsup:
> > Further, much framework/app code uses the $previous to wrap exceptions as they bubble up, > > If a programmer wants to wrap an exception in their own one let them. > No one forbids catching exceptions; they just shouldn’t be suppressed.
> > > Async\isCancellation(Throwable): bool > Why make a separate function if you can just walk through the chain?
If everyone writes their own isCancellation() we risk divergence (not to mention, it will be faster in C and basically need to be checked on every catch that might await); having one blessed function guarantees consistent detection and can allow for static-analysis support.
> > > Minor nit: in the Async\protect section, it would be nice to say that cancellations being AFTER the protect() are guaranteed, and also specify reentry/nesting of protect(). Like what happens here: > > That’s a good case! Re-entering protect should be forbidden that must > not be allowed.
<3 that's good to know! It definately needs to be in the RFC. If you don't mind me asking: why is this the case?
> > > Also, if I'm reading this correctly, a coroutine can mark itself as canceled, yet run to completion; however anyone await()'ing it, will get a CancellationException instead of the completed value? > If a coroutine is canceled, its return value will be ignored. > However, of course, it can still call return, and that will work > without any issues. > I considered issuing a warning for such behavior but later removed it, > since I don’t see it as particularly dangerous. > This point requires attention, because there’s a certain “flexibility” > here that can be confusing. However, the risk in this case is low.
I would find it surprising behaviour -- if you cancel a context in go, it may or may not complete, but you get back both the completion (if it completed) and/or the error. In C#, it throws an exception and it never completes. Languages have different ways to do it, but it should be documented in the RFC what the behaviour is and how to handle this case. Ergonomics matter as much as the feature existing. As far as observability goes, it might be a good idea to issue a notice instead of a warning. Notice is often suppressed and rarely causes any issues, but in development, seeing that would at least let me know something was going on that I should investigate.
> > > Allowing destructors to spawn feels extremely dangerous to me (but powerful). These typically -- but not always -- run between the return statement and the next line (typically best to visualize that > as the "}" since it runs in the original scope IIRC). That could make it 'feel like' methods/functions are hanging or never returning if a library abuses this by suspending or awaiting something. > > Launching coroutines in destructors is indeed a relatively dangerous > operation, but for different reasons mainly related to who owns such > coroutines. However, I didn’t quite understand what danger you were > referring to? > > Asynchronous operations, as well as coroutine launching, are indeed > used in practice. The code executes properly, so I don’t quite see > what risks there could be, apart from potential resource leaks caused > by faulty coroutines.
I think we missed each other here. Consider the following code: function test() { $r = new AsyncResource(); return 42; // destructor suspends here } Would this delay the caller's return until the destructor's coroutine finished, or is it detached? If detached, can it interleave safely with subsequent code? This should be documented in the RFC so people can plan for it and use it appropriately (such as managing transactions or locks inside destructors).
> > async.zombie_coroutine_timeout says 2 seconds in the text, but 5 seconds in the php.ini section. > Thanks. > > > What is defined as "application considered finished?" FrankenPHP workers, for instance, don’t "finish" — is there a way to reap zombies manually? > > The Scheduler keeps track of the number of coroutines being executed. > When the number of active coroutines reaches zero, the Scheduler stops > execution. Zombie coroutines are not counted among those that keep the > execution running. If PHP is running in worker mode, then the worker > code must correctly keep the execution active. But even workers > sometimes need to shutdown.
I have some workers that haven't restarted since April. :) So, having a way to manually reap zombies (much like we do with OS-level code when running as PID 1) and track them, would be nice to have. At least, as part of the scheduler API.
> > > it would be good to specify ordering and finally/onFinally execution here > Doesn’t the RFC define the order of onFinally handler execution? > onFinally handlers are executed after the coroutine or the Scope has completed. > onFinally is not directly related to dispose() in any way. > > When dispose() is called, coroutine cancellation begins. This process > may take some time. Only after the last coroutine has stopped will > onFinally be invoked. In other words, you should not attempt to link > the calls of these methods in any way.
This should be documented on the RFC, it still doesn't explain what the order of operations is though. This matters because if you are doing cleanup during disposal, you need to know what things will still be around (for reference, order of operations for GC is well documented and defined https://www.php.net/manual/en/features.gc.collecting-cycles.php which is what I'm expecting to see here).
> > > Maybe something like Async\isEnabled() to know whether I should use fibers or not. > Good idea!, > considering that such a function actually exists at the C code level. > > > Is this "same exception" mean this literally, or is it a clone? If it is the same, what prevents another code path from mutating the original exception before it gets to me? > It’s the exact same object that is, a reference to the same instance. > So if someone modifies it, those changes will, of course, take effect.
This should probably be documented in the RFC: "Exceptions and returned objects are shared objects; mutating them is undefined behavior if there are multiple awaiters."
> > > Is there also a timeout on Phase 1 shutdown? Otherwise, if it is only an exception, then this could hang forever. > > That’s true! A hang is indeed possible. I’m still not sure whether > it’s worth adding an auxiliary mechanism to handle such cases, because > that would effectively make PHP “smarter” than the programmer. I > believe that a language should not try to be smarter than the > programmer if the application runs in a certain way, then it’s > probably meant to be that way.
I think of it more as observability than smarts (esp if the timeout is configurable) ... otherwise, how would you even know if it is hanging on shutdown vs. doesn't even know it is supposed to be shutting down? I'm reminded of certain CLI tools that require me issuing a SIGTSTP (ctrl-z) to issue a SIGTERM or SIGKILL because SIGINT (ctrl-c) doesn't appear to work. If it is my program, is it that there is a bug with SIGINT handlers -- or is it hanging during shutdown? Having a timeout there would at least protect me from my customers/users getting stuck due to a bug, and I could always set the timeout to something infinite-ish (0? -1?) if that is the behaviour I want. — Rob

Edmond Dantes

315 days ago
> So, IMHO, we should probably be using the american spelling here ...
Yes, it would be nice to have some way to validate the English language. I’d definitely suggest allowing RFCs to be edited directly in Git.

Edmond Dantes

315 days ago
> This seems a bit contradictory and confuses things. When I await(), do I need to do it in a loop, or just once? > It might be a good idea to make a couple subtypes: Signal and Future. Coroutines become Future that only await once, while Signal is something that can be awaited many times.
I made a mistake in my previous response, and it requires clarification. Classes that can be awaited multiple times are indeed possible. These include `TimeInterval`, `Channel`, `FileSystemEvent`, as well as I/O triggers. All of these classes can be `Awaitable`. This is done to allow bulk waiting on objects, regardless of how they work internally. So this is meant for functions like `awaitXX`, although such behavior is also possible for the `await()` function itself: ```php $timeInterval = new Async\TimeInterval(1000); while(true) { await($timeInterval); } ``` As for objects of type `Future`, it’s clear that in future RFCs there will be a `FutureInterface`, which will be implemented by coroutines.

Aaron Piotrowski

315 days ago
> On Oct 21, 2025, at 9:56 AM, Edmond Dantes <edmond.ht@gmail.com> wrote: > >> This seems a bit contradictory and confuses things. When I await(), do I need to do it in a loop, or just once? >> It might be a good idea to make a couple subtypes: Signal and Future. Coroutines become Future that only await once, while Signal is something that can be awaited many times. > > I made a mistake in my previous response, and it requires clarification. > Classes that can be awaited multiple times are indeed possible. > These include `TimeInterval`, `Channel`, `FileSystemEvent`, as well as > I/O triggers. > > All of these classes can be `Awaitable`. > This is done to allow bulk waiting on objects, regardless of how they > work internally. > So this is meant for functions like `awaitXX`, although such behavior > is also possible for the `await()` function itself: > > ```php > $timeInterval = new Async\TimeInterval(1000); > while(true) { > await($timeInterval); > } > ``` > > As for objects of type `Future`, it’s clear that in future RFCs there > will be a `FutureInterface`, which will be implemented by coroutines.
Hi Edmond, I've been meaning to review your RFC and implementation for some time, but for various reasons, I still haven't been able to give it a thorough read and review. I noticed this portion of the discussion and wanted to drop a note now, rather than waiting until I was able to read the entire RFC. Awaitables should always represent a single value. Awaiting multiple times should never result in a different value. Async sets of values should use a different abstraction to represent a set. rxjs Observables (rxjs.dev) are on example. AMPHP has a pipeline library, https://github.com/amphp/pipeline, which defines a ConcurrentIterator interface. The latter IMO is more appropriate for PHP + fibers. I recommend having a look at how Future and ConcurrentIterator are used within AMPHP libraries. I think you should consider additional time beyond only two more weeks for discussion of this RFC before bringing it to a vote. PHP 8.6 or 9 is some time away. This is definitely not an RFC to rush to voting. Cheers, Aaron Piotrowski

Edmond Dantes

315 days ago
Hi
> Awaitables should always represent a single value. Awaiting multiple times should never result in a different value.
Where did this rule come from? In programming languages (except Rust), there is no explicit restriction on the behavior of the await operation, nor a specific requirement that it must always return the same value. However, from a usability perspective, such a rule would make the code simpler. But... On the other hand, if we restrict the behavior of await, we fail to cover the full range of possible cases — and that’s also bad.
> AMPHP has a pipeline library, https://github.com/amphp/pipeline,
That’s not quite the same. The general case of interacting with Awaitable objects looks like this: ```php // Waiting for the first event from any object in the set. // The objects in the set are of different types. awaitAny(obj1, obj2, obj3); // or All ... ``` But, programming languages don’t always implement this **general case**, and sometimes even try to avoid it altogether. And working with a data stream is implemented differently through `await foreach`. There is another way to solve this problem (all Futures only) — through a method that always returns a new Future. For example: ```php // $queue->whenReady() returns Future object awaitAll($future, $queue->whenReady()); ``` Downside: each time we create a new object in memory, while the loop still remains. At the moment, I don’t see any compelling reason to impose artificial restrictions on behavior. * Future objects are a special case of Awaitable objects, * while Awaitable objects represent the general case.
> I think you should consider additional time beyond only two more weeks for discussion of this RFC before bringing it to a vote. > PHP 8.6 or 9 is some time away. This is definitely not an RFC to rush to voting.
I have no objections. Thank you, Ed

Rob Landers

314 days ago
On Tue, Oct 21, 2025, at 19:11, Edmond Dantes wrote:
> Hi > > > Awaitables should always represent a single value. Awaiting multiple times should never result in a different value. > Where did this rule come from?
I don’t think it’s a “rule” per se and why I suggested breaking it up into two different kinds of Awaitables. Invariants make code easier to reason about and work with. The more invariants you have, the easier it is to form, maintain, and refactor. — Rob

Edmond Dantes

314 days ago
Hi
> I don’t think it’s a “rule” per se and why I suggested breaking it up into two different kinds of Awaitables. > Invariants make code easier to reason about and work with. The more invariants you have, the easier it is to form, maintain, and refactor.
So that is the rule: **invariants make code easier to understand**. A more general principle is stated as follows: reducing complexity. I think this point needs some thought. If the await operation is allowed only for Future, it will make the code more consistent which is a good thing. Then it will be necessary to add a FutureInterface, and Awaitable should be hidden from the UserLand namespace.

Rob Landers

314 days ago
On Wed, Oct 22, 2025, at 06:32, Edmond Dantes wrote:
> Hi > > > I don’t think it’s a “rule” per se and why I suggested breaking it up into two different kinds of Awaitables. > > Invariants make code easier to reason about and work with. The more invariants you have, the easier it is to form, maintain, and refactor. > > So that is the rule: **invariants make code easier to understand**. > A more general principle is stated as follows: reducing complexity. > > I think this point needs some thought. > If the await operation is allowed only for Future, it will make the > code more consistent which is a good thing. > > Then it will be necessary to add a FutureInterface, and Awaitable > should be hidden from the UserLand namespace.
A simpler solution might be to keep things as they are, but have the non-idempotent constructs be generators of Awaitable instead of non-idempotent Awaitables. This would basically mean just changing some text in the RFC and some implementations that aren't documented in the RFC (as far as I can tell). — Rob

Edmond Dantes

314 days ago
> A simpler solution might be to keep things as they are, but have the non-idempotent constructs be generators of Awaitable instead of non-idempotent Awaitables
So it’s the same as in C#? The problem with this situation is that so far I haven’t been able to find any cases proving that a non-Future object could cause serious failures in the code. At the same time, if a select case expression is introduced in the future, it would make more sense for select to work with an awaitable object rather than a Future. That’s why I’m not yet sure it’s worth abandoning the general behavior unless a convincing argument is found showing that it leads to real problems. -- Ed

Rob Landers

314 days ago
On Wed, Oct 22, 2025, at 10:34, Edmond Dantes wrote:
> > A simpler solution might be to keep things as they are, but have the non-idempotent constructs be generators of Awaitable instead of non-idempotent Awaitables > So it’s the same as in C#? > > The problem with this situation is that so far I haven’t been able to > find any cases proving that a non-Future object could cause serious > failures in the code. > > At the same time, if a select case expression is introduced in the > future, it would make more sense for select to work with an awaitable > object rather than a Future. > > That’s why I’m not yet sure it’s worth abandoning the general behavior > unless a convincing argument is found showing that it leads to real > problems. > > -- Ed >
The example I gave is probably a good one? If I'm writing framework-y code, how do I decide to await once, or in a loop? In other words, how do I detect whether an Awaitable is idempotent or will give a different result every time? If I'm wrong, I could end up in an infinite loop, or missing results. Further, how do I know whether the last value from an Awaitable is the last value? I think if you could illustrate that in the RFC or change the semantics, that'd be fine. — Rob

Aaron Piotrowski

315 days ago
> On Oct 21, 2025, at 9:56 AM, Edmond Dantes <edmond.ht@gmail.com> wrote: > >> This seems a bit contradictory and confuses things. When I await(), do I need to do it in a loop, or just once? >> It might be a good idea to make a couple subtypes: Signal and Future. Coroutines become Future that only await once, while Signal is something that can be awaited many times. > > I made a mistake in my previous response, and it requires clarification. > Classes that can be awaited multiple times are indeed possible. > These include `TimeInterval`, `Channel`, `FileSystemEvent`, as well as > I/O triggers. > > All of these classes can be `Awaitable`. > This is done to allow bulk waiting on objects, regardless of how they > work internally. > So this is meant for functions like `awaitXX`, although such behavior > is also possible for the `await()` function itself: > > ```php > $timeInterval = new Async\TimeInterval(1000); > while(true) { > await($timeInterval); > } > ``` > > As for objects of type `Future`, it’s clear that in future RFCs there > will be a `FutureInterface`, which will be implemented by coroutines.
Hi Edmond, I've been meaning to review your RFC and implementation for some time, but for various reasons, I still haven't been able to give it a thorough read and review. I noticed this portion of the discussion and wanted to drop a note now, rather than waiting until I was able to read the entire RFC. Awaitables should always represent a single value. Awaiting multiple times should never result in a different value. Async sets of values should use a different abstraction to represent a set. rxjs Observables (rxjs.dev) are on example. AMPHP has a pipeline library, https://github.com/amphp/pipeline, which defines a ConcurrentIterator interface. The latter IMO is more appropriate for PHP + fibers. I recommend having a look at how Future and ConcurrentIterator are used within AMPHP libraries. I think you should consider additional time beyond only two more weeks for discussion of this RFC before bringing it to a vote. PHP 8.6 or 9 is some time away. This is definitely not an RFC to rush to voting. Cheers, Aaron Piotrowski

A.L.E.C

314 days ago
On 5.10.2025 07:23, Edmond Dantes wrote:
> https://wiki.php.net/rfc/true_async
The RFC is not easy to process. Here's some ideas. - The glossary in "Overview" is good, but probably incomplete. The examples there, with no description, do not help much and could be removed, imo. - "Collable by design" and "Coroutine lifetime" sections should become subsections of "Coroutine" or placed after it. - the "Scheduler and Reactor" section does not explain much over what's in the glossary. - the "Critical section" section should not be a main section, "Cancellation policy" probably either. - the "Basic usage" section in the "Suspension" section is useless. - don't use "suspend keyword" and "await keyword", they are functions. One question. Seems like we don't really need delay() function. Why not add an argument to the suspend() function? I think it would make the code easier to understand, considering seeing suspend(1000) versus delay(1000).
-- Aleksander Machniak Kolab Groupware Developer [https://kolab.org] Roundcube Webmail Developer [https://roundcube.net] ---------------------------------------------------- PGP: 19359DC1 # Blog: https://kolabian.wordpress.com

Edmond Dantes

314 days ago
Hello.
> - The glossary in "Overview" is good, but probably incomplete. The > examples there, with no description, do not help much and could be > removed, imo.
Do I understand correctly that I should remove the examples without descriptions? Or would it be better to add descriptions to them? Although the last examples might not be very illustrative or easy to grasp.
> the "Scheduler and Reactor" section does not explain much over what's in the glossary.
What else do you think could be added? The internal implementation doesn’t belong in the scope of the RFC, it can change. They don’t have any special API in the PHP userland. The reactor can only be used directly at the C/C++ level, meaning within a PHP extension. It’s also intentionally impossible to directly affect the Scheduler’s behavior.
> One question. Seems like we don't really need delay() function. Why not > add an argument to the suspend() function? I think it would make the > code easier to understand, considering seeing suspend(1000) versus > delay(1000).
That didn’t occur to me. Interesting idea. You’re saying it would make the code more readable. But wouldn’t it be confusing since the functions have slightly different semantic purposes? As a non-native English speaker, I don’t really feel the difference between delay and suspend. They seem close in meaning. But is that the case for others? Thanks, Ed.

A.L.E.C

314 days ago
On 22.10.2025 19:01, Edmond Dantes wrote:
>> - The glossary in "Overview" is good, but probably incomplete. The >> examples there, with no description, do not help much and could be >> removed, imo. > > Do I understand correctly that I should remove the examples without > descriptions?
Examples in the Overview section aren't very helpful. I would remove them from there, but maybe some of them need to appear later. I didn't read it that carefully to suggest precise changes.
> Or would it be better to add descriptions to them? > Although the last examples might not be very illustrative or easy to grasp. > >> the "Scheduler and Reactor" section does not explain much over what's in the glossary. > > What else do you think could be added? > The internal implementation doesn’t belong in the scope of the RFC, it > can change. > They don’t have any special API in the PHP userland. > The reactor can only be used directly at the C/C++ level, meaning > within a PHP extension. > It’s also intentionally impossible to directly affect the Scheduler’s behavior.
I think that it might be better to not mention them at all in the RFC. Or better separate parts that describe userland and engine. The structure of the Proposal is a bit chaotic.
-- Aleksander Machniak Kolab Groupware Developer [https://kolab.org] Roundcube Webmail Developer [https://roundcube.net] ---------------------------------------------------- PGP: 19359DC1 # Blog: https://kolabian.wordpress.com

Edmond Dantes

313 days ago
Hi,
> I think that it might be better to not mention them at all in the RFC. > Or better separate parts that describe userland and engine.
Different people have different opinions on this point. But it seems there’s no harm in PHP developers knowing that these two components exist under the hood. Their description in the RFC isn’t part of the implementation details. It’s more of a mention in the context of concurrency architecture. The descriptions are given in the most abstract way possible and don’t include any implementation details. --- Ed

Edmond Dantes

313 days ago
The volume of the discussion seems to have become too complex for future processing. Therefore, I tried to organize the proposals into specific tasks. https://github.com/true-async/php-true-async-rfc/issues I think this format will also be useful for those who want to see what changes will be made and why they are being made.

Edmond Dantes

309 days ago
Hello all. ### Current work and discussion plan for this RFC 1. By the end of this week, the proposed changes at [https://github.com/true-async/php-true-async-rfc/issues](https://github.com/true-async/php-true-async-rfc/issues) will be accepted if no objections are raised. 2. After that, the RFC document will be updated, and a new **2-week discussion period** will begin. 3. After the new changes are accepted, the RFC will be updated again, and the process will repeat. The discussion will be extended as long as necessary, including at the request of the participants. --- With best regards, Ed

Larry Garfield

309 days ago
On Mon, Oct 27, 2025, at 7:08 AM, Edmond Dantes wrote:
> Hello all. > > ### Current work and discussion plan for this RFC > > 1. By the end of this week, the proposed changes at > > [https://github.com/true-async/php-true-async-rfc/issues](https://github.com/true-async/php-true-async-rfc/issues) > will be accepted if no objections are raised. > > 2. After that, the RFC document will be updated, and a new **2-week > discussion period** will begin. > > 3. After the new changes are accepted, the RFC will be updated again, > and the process will repeat. > > The discussion will be extended as long as necessary, including at the > request of the participants. > > --- > With best regards, > Ed
I am not sure how feasible this is, but would there be a way to split the "async toggle" of IO operations off to its own PR/RFC? To me, that is by far the most important part of this RFC as that's the biggest blocker for wider async adoption, but I'm not sure how many layers are needed above it to make it possible to toggle in a safe fashion. --Larry Garfield

Alexandru Pătrănescu

309 days ago

Larry Garfield

309 days ago
On Mon, Oct 27, 2025, at 11:18 AM, Alexandru Pătrănescu wrote:
>> >> >> On Mon, Oct 27, 2025 at 6:05 PM Larry Garfield <larry@garfieldtech.com> wrote:
>>> I am not sure how feasible this is, but would there be a way to split the "async toggle" of IO operations off to its own PR/RFC? To me, that is by far the most important part of this RFC as that's the biggest blocker for wider async adoption, but I'm not sure how many layers are needed above it to make it possible to toggle in a safe fashion. >> >> Hi! >> >> Can you clarify what you mean by "async toggle"? >> Is it the actual implementation that would use async constructs if the current context is a coroutine for each implementation of IO functions? >> Yes, for that, it would be nice to have separate PRs, even multiple ones for easier review. But maybe you mean something else... >> >> -- >> Alex
The most important feature of this RFC, IMO, is that when async is "active", all IO operations become non-blocking and automatically suspend an active coroutine, so that other coroutines can act. That means you can write file_get_contents() or whatever, and in non-async land it will block as normal, but in async land it will suspend and let other coroutines run, then pick up again when ready, without requiring any code changes. (At least that's how I understand that part of the RFC.) That is *huge*, and easily the most important feature. Really, if we had that in core it would be possible to do most of the rest in user-space with the existing Fibers, I suspect. But I don't know how feasible it is to separate that part out, in large part because it would mean exposing some kind of way to toggle if async is "active" (for some definition of active). But if that is possible/feasible, that would be a much narrower, more easily reviewable, and still highly useful RFC that could be iterated on in both user space and core. I do not know how coupled that is to the new Fiber-incompatible loop, which is the biggest problem. --Larry Garfield

Edmond Dantes

309 days ago
Hello
> I am not sure how feasible this is, but would there be a way to split the "async toggle" of IO operations off to its own PR/RFC? > To me, that is by far the most important part of this RFC as that's the biggest blocker for wider async adoption, > but I'm not sure how many layers are needed above it to make it possible to toggle in a safe fashion.
Why is this considered the main blocker? (Or maybe I didn’t quite catch the meaning.) After all, this is precisely the part of the RFC that doesn’t actually change the behavior from the user’s perspective inside a coroutine. As for the PRs. There’s no doubt there will be several. I hope the PHP core team will help with the code separation process and advise on the best way to do it, since it’s not a trivial task. As for the toggle switch. If a developer doesn’t use spawn, then why toggle anything at all? And even if they do, there’s still no difference — the code won’t start executing “differently.” --- Best Regards, Ed

Edmond Dantes

309 days ago
> I am not sure how feasible this is, but would there be a way to split the "async toggle" of IO operations off to its own PR/RFC? > To me, that is by far the most important part of this RFC as that's the biggest blocker for wider async adoption, > but I'm not sure how many layers are needed above it to make it possible to toggle in a safe fashion.
Ah, I think I understand what you mean. Yes, from an implementation standpoint, non-blocking behavior is provided by the Scheduler API and Reactor API. These two APIs are always available anywhere — in the core, extensions, and so on. However, this isn’t part of the RFC itself, as it belongs to implementation details. Moreover, do you remember the first version of the RFC? It had a function that explicitly started the Scheduler. So indeed, in that first version PHP could be in two states: synchronous and asynchronous. What’s the difference in this RFC? PHP is **always** in an asynchronous state. Even when executing index.php, you can think of it as running inside a coroutine. There’s just a single coroutine, so no switching occurs (although an extension could, for example, create another coroutine — which is perfectly valid). You keep writing your code exactly as before. You don’t need to think about PHP being “asynchronous” now. As long as you’re writing code inside a single coroutine, you’re still writing synchronous code. -- Ed

Rob Landers

309 days ago
On Mon, Oct 27, 2025, at 18:53, Edmond Dantes wrote:
> > I am not sure how feasible this is, but would there be a way to split the "async toggle" of IO operations off to its own PR/RFC? > > To me, that is by far the most important part of this RFC as that's the biggest blocker for wider async adoption, > > but I'm not sure how many layers are needed above it to make it possible to toggle in a safe fashion. > > Ah, I think I understand what you mean. > Yes, from an implementation standpoint, non-blocking behavior is > provided by the Scheduler API and Reactor API. These two APIs are > always available anywhere — in the core, extensions, and so on. > > However, this isn’t part of the RFC itself, as it belongs to > implementation details. Moreover, do you remember the first version of > the RFC? It had a function that explicitly started the Scheduler. So > indeed, in that first version PHP could be in two states: synchronous > and asynchronous. > > What’s the difference in this RFC? > PHP is **always** in an asynchronous state. Even when executing > index.php, you can think of it as running inside a coroutine. There’s > just a single coroutine, so no switching occurs (although an extension > could, for example, create another coroutine — which is perfectly > valid). > > You keep writing your code exactly as before. You don’t need to think > about PHP being “asynchronous” now. As long as you’re writing code > inside a single coroutine, you’re still writing synchronous code. > > -- Ed
As far as I understand, the only thing missing from php-src that would allow everything to be async is a Fiber scheduler. We already have tons of Fiber libraries out there ... but just no unified scheduler. If we had one of those, then making Fiber-aware i/o functions is "trivial" compared to implementing this async RFC. If we’re going to merge just a scheduler/reactor, it might make more sense to implement a Fiber scheduler than a completely new way of doing async. — Rob

Edmond Dantes

309 days ago
Hello! There are posts online, specifically in this discussion, from the Swoole owner and the Swow author. These are technically well-written messages that clearly and precisely explain why Fiber was a premature solution. It has been four or five years since then, and it is surprising that this is still not clear to everyone. Is it possible to implement a Reactor and Scheduler in PHP? Of course. Programming is a colorful world filled with magical creatures such as griffin-foxes, turtle-ravens, and octocats. The main thing is to stop in time :)

Jakub Zelenka

308 days ago
On Mon, Oct 27, 2025 at 7:14 PM Rob Landers <rob@bottled.codes> wrote:
> > As far as I understand, the only thing missing from php-src that would > allow everything to be async is a Fiber scheduler. We already have tons of > Fiber libraries out there ... >
I'm not sure if Fibers were particular success. They are quite hard to use and you need an extra library like amp so I think it would be useful to give users a solution that is available in the core. I think this would give it a bit more trust as it will also get the security guarantees (standard core support for handling security issues). It will take time and there will be multiple RFC's to get there and the implementation will also require thorough review. So splitting that to smaller pieces is quite important. That said I'm planning introduction of better API for polling (almost done and soon to be announced) that will be using special polling handles that will be available for streams, sockets, curl and possible other extensions. Streams should then get special notification callbacks that would be called before IO and most likely provide the polling handle as a parameter so this could be used by user space in their own reactor / scheduler. I plan to also provide a similar internal API so async can integrate cleanly into this without overwriting stream polling function as it's the case in the current implementation. Similar API should be also provided for curl (Joe already created part of it), sockets and possible other exts. In other words, the user space should be able to gain this functionality and the async core extension would be more core internal user of it. The above is just for sockets and there will be needed further work for file IO. I'm preparing new PHP IO internal API that will be initially mainly for copying but will allow extension for other operations including file operations and will use io_uring on Linux. I'm still not sure how to best expose it to user space as the ring entry completion don't have usual polling flow so it does not exactly fit into those callbacks. We could possible do some sort pipe and thread that would process the completion queue but maybe there is a better solution. This is still TBD but usually file IO is not the main IO blocker so this can come a bit later. We will also need to think about DNS resolving that might be even trickier and might require IO thread pools - those might be needed as a backup solution for platform that don't support io_uring anyway. I think those API's will be also prerequisites for this async to implement this functionality as I'm not fond of adding some hacks and functionality duplication (that would be quite a pain for maintenance). Kind regards, Jakub

Edmond Dantes

309 days ago
PUML Link here: https://editor.plantuml.com/uml/TLBDRjGm4BxFKunw0QJTvSu1LQfQgH9L4HLmTftPpQYE7SrCkXigtXqx8MxOYfF7zZVVZyUNQaviw0Be4yVUYUimS2GRUy97-iKa0COM2B-HyvPasmW_KyIh96cm3CMxr5004FBcuY4ZBwvFvFDTAgXeT39yVyEF91ykq6azUm74LLCbd41r1x__eNxmBJL389bGTOSlPxZPxOpwMvyBNkSOXbzIwWjgAWh9Exm9wOXfZxJ4WDUms-tdolS9tT6nuUt7UwH21ijDHbLl1HUJyNv48TUCw6kqIlkcOMGA3QQ8aKusom1a5aBXGsl5NOL3hJ9rD4b1NpKmy9xyw0FjuDPG1-qfDYk05fKvXuiD2kdGaOArrE6nfLZJ2lL9JASGfKztGBcXc3gpjamOtlw3DeKi_kCErPpH1lBYdpQJyjNNxoXqO3KItQtUPXu3AHxPMex8zb_bnMmTH9SYvrLnpu6m8VN2VJdOW76NXMPjvKDqGV6P7Tu_xE1d2UxYF5LCtWy5oJOFacdryrPUBdCrTE4F

Jakub Zelenka

309 days ago
Hi, On Sun, Oct 5, 2025 at 7:26 AM Edmond Dantes <edmond.ht@gmail.com> wrote:
> Good day, everyone. I hope you're doing well. > > I’m happy to present the fourth version of the RFC. It wasn’t just me > who worked on it — members of the PHP community contributed as well. > Many thanks to everyone for your input! > > https://wiki.php.net/rfc/true_async
I just re-read it again with all the feedback provided and I think it should get further stripped as it seems problematic to get an agreement on all of this and properly discuss it on ML all the details. I think this (or more v5) should strip the following: - exposing Scope and all operations in it. It means it should allow using only the default scope in this version. That should significantly reduce the size of the RFC as it removes structured concurrency and other parts related to scopes (including the reduction of error handling logic). - timer functions could also be removed even though it will make it less usable but the point is to make it as small as possible and those are not absolutely essential parts. - critical section should be stripped as well - nginx unit example should be removed as it might be confusing - I understand why it was added but it might be more confusing than useful - drop php.ini setting and just use default for now The idea is to make this as small as possible so this might be possible to discuss and get actually more people to read the RFC (this is just too long). This will also allow to concentrate on specific pieces like for example deciding whether to use FutureLike or Awaitable. We just had a chat about this work during our PHP Foundation and the agreement seems to be that this should be reduced and come in smaller pieces to be able to better figure out what's actually useful for PHP. Another point was also to make clear that this proposal is not meant to introduce a new "right" way to use PHP but it's actually useful everywhere. We actually discussed this with Edmond privately and agreed that this is useful for FPM as well and he even created an example proving it. So we should just try to make it clearer in the RFC as there was some confusion in the discussion. Kind regards, Jakub

Edmond Dantes

309 days ago
Hello.
> I think this (or more v5) should strip the following > - exposing Scope and all operations in it. It means it should allow using only the default scope in this version. That should significantly reduce the size of the RFC as it removes structured > concurrency and other parts related to scopes (including the reduction of error handling logic). > - timer functions could also be removed even though it will make it less usable but the point is to make it as small as possible and those are not absolutely essential parts. > - critical section should be stripped as well > - nginx unit example should be removed as it might be confusing - I understand why it was added but it might be more confusing than useful > - drop php.ini setting and just use default for now
I suppose that’s exactly what we’ll do. However, I won’t completely remove Scope. I’ll move it to a separate document in the WIKI, so it can be easily referenced later. --- Thank you, Ed