Inside the True Async RFC: PHP's Long Road to Native Coroutines
A clear look at PHP's True Async RFC, the spawn and await coroutine model, why version 1.6 was voted down, and where native async is heading for PHP 8.6.
For years, the answer to “how do I run concurrent code in PHP” has been “install something.” Swoole, Swow, AMPHP, ReactPHP. Each is excellent, and each lives outside the language. The True Async RFC, authored by Edmond (edmondifthen) and currently sitting at version 1.7, is the most serious attempt yet to bring coroutines into PHP core itself. It went to a vote earlier this year, did not pass, and has since been reworked. If you write long-running PHP, this is the proposal worth understanding now, because the direction it sets will shape the runtime for years.
What the RFC Actually Proposes
The headline idea is deceptively simple. True Async introduces a new Async namespace with a handful of primitives: spawn() to start a coroutine, await() to wait for its result, and suspend() to hand control back to the scheduler. A Coroutine class represents the unit of execution, and two interfaces, Awaitable and Completable, describe things you can wait on.
The part that matters most for everyday code is what does not change. Functions like file_get_contents(), fopen(), and mysqli_query() keep their exact signatures. There is no requirement to sprinkle async and await keywords through every layer of your stack the way you would in JavaScript or C#. You write what looks like ordinary sequential code, run it inside a coroutine, and the runtime handles the switching.
use function Async\spawn;
$coroutine = spawn('file_get_contents', 'https://php.net');
echo "this line runs immediately, line " . __LINE__ . "\n";
Here spawn() launches file_get_contents in a separate execution context and returns instantly. The echo runs before the download finishes. The coroutine resumes when the scheduler gives it a turn. That is the whole pitch: concurrency with minimal changes to existing code.
spawn, await, and suspend
await() is how you collect a result. It suspends the calling coroutine until the awaited one finishes, then returns its value or rethrows whatever exception it raised.
use function Async\spawn;
use function Async\await;
function readFile(string $name): string
{
$result = file_get_contents($name);
if ($result === false) {
throw new Exception("Error reading {$name}");
}
return $result;
}
echo await(spawn(readFile(...), 'report.txt'));
suspend() is the manual lever. A coroutine can yield control at any point, letting the scheduler run something else before resuming it.
use function Async\spawn;
use function Async\suspend;
function greet(string $name): void {
echo "Hello, {$name}!\n";
suspend();
echo "Goodbye, {$name}!\n";
}
spawn(greet(...), 'World');
spawn(greet(...), 'Universe');
The expected output interleaves the two coroutines: both greetings print first, then both farewells. One important caveat the RFC repeats firmly is that you must never guess when a coroutine resumes or which one the scheduler picks next. The scheduling algorithm is deliberately not part of the contract and can change, including via third-party extensions.
A nice touch is that the main script body is itself a coroutine. So spawn(), await(), and suspend() work at the top level, not just inside nested async functions.
Note: the non-blocking versions of functions like
file_get_contentsare not defined in this RFC. The base proposal sets the rules for coroutines and leaves the specific I/O behavior of individual functions to follow-up RFCs.
Cancellable by Design
The most opinionated decision in the proposal is its cancellation model. By default, any coroutine waiting on an operation can be cancelled from the outside at any moment. Cancellation surfaces as a Cancellation exception, which deliberately extends \Throwable directly rather than \Exception, so a stray catch (\Exception) will not silently swallow it.
$coroutine = spawn(function () {
// long-running work
});
$coroutine->cancel(new \Cancellation('Task was cancelled'));
The reasoning is pragmatic. PHP workloads are read-heavy, and reads (database queries, API calls, file reads) generally do not mutate state, so cancelling them is safe. Making everything cancellable by default removes a mountain of boilerplate. The flip side is your responsibility: code that performs writes or holds a transaction open must handle the Cancellation exception, and the RFC recommends finally blocks for cleanup rather than catching Cancellation yourself.
use function Async\finally;
use function Async\spawn;
function task(): void
{
$file = fopen('data.txt', 'r');
finally(fn() => fclose($file));
// even if this coroutine is cancelled, the file is closed
throw new Exception('boom');
}
spawn('task');
There is also a deadlock detector. If no coroutines can make progress and the event loop is empty, the runtime enters graceful shutdown and ultimately throws a DeadlockError as a diagnostic, pointing at the lines where coroutines were stuck.
Fibers Are Still Here
PHP 8.1 gave us Fibers, and True Async does not throw them away. Fibers are treated as a kind of coroutine, and the RFC adds a single method, Fiber::getCoroutine(), to bridge the two. Existing Fiber-based libraries keep working unchanged, and getCoroutine() simply returns null when a fiber runs outside an async context. That backward compatibility promise is part of why the proposal claims no required changes to existing extensions or SAPIs, from CLI to FPM.
Why It Did Not Pass, and What Comes Next
Version 1.6 of the RFC went to a formal vote that closed on February 16, 2026. It needed a two-thirds majority and did not get there, so it was rejected. The opposition, by most accounts, was not about whether PHP should have async. It was about how: concerns over the memory model, the size and ambition of a single proposal, and how much behavior should live in core versus an extension.
Edmond’s response has been to keep iterating rather than abandon the effort. The proposal was split so the base RFC now covers only the core primitives (coroutines, await, cancellation), while structured concurrency moved into a separate Scope RFC, and the low-level engine hooks became a distinct TrueAsync API RFC aimed at letting extensions provide non-blocking I/O in C, Rust, or C++. As of its last update in early April 2026, the base RFC is back under discussion and still targets PHP 8.6. There is even a working FrankenPHP integration that handles multiple concurrent HTTP requests in a single thread by spawning a coroutine per request.
So where does that leave a working PHP developer in mid-2026? Native async is not landing tomorrow, and you should keep reaching for Swoole, AMPHP, or ReactPHP for production concurrency today. But the conversation has matured from “should PHP do this” to “exactly how should PHP do this,” and the API in front of us, spawn, await, suspend, and cancellable-by-default coroutines, is concrete enough to read and reason about. Whether the syntax survives the next round of votes intact is an open question, which is exactly why it is worth following the externals threads now rather than after the fact.