PHP at Scale #19
Async PHP in 2026
Welcome to the 19th edition of PHP at Scale. I am diving deep into the principles, best practices and practical lessons learned from scaling PHP projects - not only performance-wise but also code quality and maintainability.
This edition is about async PHP - not “can PHP do async?” (it can, and has been able to for years), but which of the several mature approaches should you actually reach for in 2026, and what does each one look like in practice.
I recently published a post about a ReactPHP WebSocket server we’ve been running in production since 2019. Six years, thousands of connections a day, flat memory. The reaction I keep getting is surprise - people still assume PHP can’t do long-running processes. So consider this edition the broader answer: here’s the full landscape of async PHP in 2026, from the low-level primitive all the way up to runtime-level solutions that require no async code at all.
PHP Ecosystem News
API Platform now supports MCP - If you’re on Symfony + API Platform, your existing REST API can now be exposed as typed tools for AI agents (Claude, GPT, etc.) with minimal work. It reuses your existing state processors and serialization - no separate integration layer. Worth a look if your clients are starting to ask about agentic access to your APIs. I do not use API Platform in any of our projects, and there seems to be a retreat from MCP on the market, and a switch to the CLI+Skill approach, yet it might be very useful in many situations.
Symfony TUI component announced: Fabien announced a new first-party component for building rich terminal UIs in PHP: styled widgets, Twig-based layouts, and mouse support. It runs on Fibers + Revolt under the hood, which ties neatly into this edition’s topic. If you needed a signal that Fibers are now a mainstream PHP primitive - a new official Symfony component building entirely on them, already powering Fabien’s own AI coding agent in production, is that signal. I've been using modern TUIs a lot recently and am eager to write some on my own with this component!
PHP 8.1 introduced Fibers - the low-level primitive that underpins most of what modern async PHP libraries do. Understanding them takes five minutes and will make everything else in this edition click.
A Fiber is a stackful coroutine. It’s not a thread - it doesn’t run in parallel. It’s a function that can pause itself mid-execution and hand control back to the caller, then resume exactly where it left off. The caller decides when to resume it.
$fiber = new Fiber(function(): void {
echo "Fiber started\n";
$value = Fiber::suspend('pausing now');
echo "Fiber resumed with: $value\n";
});
$suspended = $fiber->start(); // prints "Fiber started", returns 'pausing now'
echo "Main got: $suspended\n"; // prints "Main got: pausing now"
$fiber->resume('hello'); // prints "Fiber resumed with: hello"The interesting part is what you can build on top of this. A simple scheduler that runs two fibers cooperatively:
$fibers = [
new Fiber(function() { echo "A\n"; Fiber::suspend(); echo "A2\n"; }),
new Fiber(function() { echo "B\n"; Fiber::suspend(); echo "B2\n"; }),
];
foreach ($fibers as $f) $f->start(); // A, B
foreach ($fibers as $f) $f->resume(); // A2, B2Output: A, B, A2, B2 - two functions interleaved by an explicit scheduler, not by the OS. This is cooperative multitasking: nothing runs until you tell it to.
The key thing to take away: Fibers alone don’t give you async I/O. They give you the ability to suspend and resume, but something still needs to watch the network sockets and decide which fiber to wake up when data arrives. That something is an event loop. Which brings us to the next layer.
For the full Fiber API and state machine, PHP.Watch has the best reference. For a practical guide on the Revolt event loop, this post on fsck.sh is worth your time.
Asynchronous and concurrent HTTP requests in PHP
Before reaching for a full event loop library, it’s worth knowing that Guzzle already supports concurrent HTTP out of the box - no event loop, no new dependencies. If your use case is “fire a batch of HTTP calls and wait for all of them,” you might already have everything you need.
This article benchmarks three approaches on real OpenAI API calls:
Method Test 1 Test 2 Test 3
Synchronous Guzzle 7.54s 6.89s 6.93s
Guzzle Async 1.29s 1.89s 1.66s
AMPHP 3.33s 2.58s 2.42sGuzzle Async uses curl_multi_* under the hood - not fibers, not an event loop - but the concurrency is real, and in this test it actually outperformed AMPHP. The author is honest about this being limited testing and results depending on the use case. That tracks with what I’ve seen: for pure HTTP fan-out, Guzzle’s async model is often enough, and the operational overhead is zero.
Where Guzzle falls short is anything beyond HTTP - timers, sockets, database queries and mixing I/O types. That’s where a proper event loop earns its place. Amp v3 is fully fiber-native - gone are the generators, the yield, and the Promise objects from v2. The API now looks like synchronous code:
use Amp\Future;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
$client = HttpClientBuilder::buildDefault();
$urls = [
'https://api.example.com/users',
'https://api.example.com/orders',
'https://api.example.com/products',
'https://api.example.com/stats',
];
$futures = array_map(
fn($url) => async(fn() => $client->request(new Request($url))),
$urls
);
[$errors, $responses] = Future\awaitAll($futures);
ReactPHP follows the same model with a longer history and slightly more promise-based API, but since v4, it ships fiber-backed async()/await() helpers that look similar. The WebSocket server I mentioned at the top is ReactPHP - one event loop, persistent TCP connections on one side, RabbitMQ on the other. Both Amp and ReactPHP share one important constraint: the event loop is single-threaded. Every callback must be fast and non-blocking. Put a synchronous PDO query inside, and the entire loop stalls for everyone.
Let’s now have a deeper look into something more useful - database queries.
Let’s Demystify Swoole: Coroutines
This is where async MySQL enters the picture - and where things get genuinely interesting.
Swoole is a C++ extension that replaces PHP’s I/O layer entirely. Its key trick for existing codebases is runtime hooks: with SWOOLE_HOOK_ALL, Swoole patches PHP’s built-in functions - including PDO - to become non-blocking transparently. Your existing query code works as-is; Swoole just makes it cooperative under the hood.
Co\run(function() {
$wg = new Swoole\Coroutine\WaitGroup();
$results = [];
$queries = [
"SELECT SLEEP(0.2), COUNT(*) FROM ... WHERE user_id BETWEEN 1 AND 20",
"SELECT SLEEP(0.2), COUNT(*) FROM ... WHERE user_id BETWEEN 21 AND 40",
"SELECT SLEEP(0.2), COUNT(*) FROM ... WHERE user_id BETWEEN 41 AND 60",
"SELECT SLEEP(0.2), COUNT(*) FROM ... WHERE user_id BETWEEN 61 AND 80",
"SELECT SLEEP(0.2), COUNT(*) FROM ... WHERE user_id BETWEEN 81 AND 100",
];
foreach ($queries as $i => $sql) {
$wg->add();
go(function() use ($i, $sql, &$results, $wg) {
$pdo = new PDO('mysql:host=db;dbname=app', 'user', 'pass');
$results[$i] = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
$wg->done();
});
}
$wg->wait();
}, SWOOLE_HOOK_ALL);Five standard PDO queries, running concurrently. No special async query API, no new driver. WaitGroup should feel familiar if you’ve used Go: add() before spawning a coroutine, done() when it finishes, wait() to block until all are complete. Note for Swoole 5: SWOOLE_HOOK_PDO_MYSQL was merged into SWOOLE_HOOK_ALL - the standalone constant no longer exists.
What I didn’t expect when preparing this edition: you don’t need Swoole to get async MySQL. amphp/mysql runs entirely in userland on Fibers and the Revolt event loop - the same stack you saw in the HTTP example, just pointing at a database. No extension, no special PHP build, composer require amphp/mysql:
use Amp\Mysql\MysqlConfig;
use Amp\Mysql\MysqlConnectionPool;
use function Amp\async;
use function Amp\Future\await;
$config = MysqlConfig::fromString('host=mysql;user=root;password=secret;db=app');
$pool = new MysqlConnectionPool($config);
$futures = [];
$queries = [...];
foreach ($queries as $i => $sql) {
$futures[$i] = async(fn() => $pool->query($sql)->fetchRow());
}
$results = await($futures);The same async()/await() pattern from the HTTP section. Once you’ve learned the Amp concurrency model, it transfers directly to database queries.
I ran both against the same setup (MySQL 8.0, PHP 8.3, 5 queries each with SLEEP(0.2) simulating a moderately slow query): Sequential PDO took 1.027s, Amp 0.225s, and Swoole 0.208s.
The gap between Swoole and Amp them is about 8% - Swoole hooks at the C level, so coroutine suspension overhead is minimal; amphp does the same work in userland and pays a small price for it. One interesting detail from the amphp run: results arrive out of insertion order - Query 3 might finish before Query 1. That’s genuine concurrent execution. Swoole’s WaitGroup with an indexed array preserves submission order artificially.
For most teams, the amphp result is the more useful number: near-identical concurrency win, standard pdo_mysql extension you already have. Swoole is the right call when you need every millisecond and have the ops capacity for a C++ extension in your pipeline. For runnable Docker examples of the PDO hook pattern, swoole-by-examples on GitHub is the best hands-on resource I’ve found.
Long-Running PHP in Production: WebSocket Servers That Never Sleep
This is a post we published at Accesto. It covers something that most async PHP guides skip: what happens after you deploy.
Long-running PHP processes hit a different class of problems than short-lived request/response cycles. Memory leaks accumulate without explicit management. You need graceful shutdown - SIGTERM handling that completes in-flight work before the process exits. Health checks so your orchestrator knows the process is actually healthy, not just alive. Watchdog timers that trigger restarts at memory thresholds before things go wrong.
WebSockets are the hardest case in this category: persistent connections, each carrying state, potentially thousands open at once. The server in that post has been running in production since 2019 - not because the first version was perfect, but because we learned these operational lessons the hard way.
The connection to everything above is direct: whether you’re using ReactPHP, Swoole coroutines, or amphp, you’re running a long-lived PHP process. The async model is just the beginning. The operational model - memory management, graceful restarts, monitoring - is what determines whether it runs reliably at 3 am on a Tuesday.
Recently, we hid a huge memory leak after a PHP upgrade. It took us a while to figure out that the upgraded PHP throws some deprecation errors, which are gathered by the Symfony error handler. It grows rapidly at a rate of 50mb each 5 minutes.
The ecosystem fragmentation is real - Swoole, Amp, and ReactPHP all solve the same problem with incompatible APIs and separate event loops. PHP internals tried to address this with a draft RFC proposing native spawn(), await(), and suspend() as language-level primitives. The vote closed in February 2026 with zero votes cast. Still Draft, no roadmap placement.
Zero votes isn’t necessarily a death sentence - it often means the RFC needs more design work rather than that the community opposes the direction. And someone didn’t wait for internals anyway. TrueAsync is a C extension that implements exactly this model today. Version 0.6.0 patches over 70 standard library functions - file I/O, cURL, PDO, sockets - to run non-blocking transparently. The API looks like this:
$group = new TaskGroup();
foreach ($files as $file) {
$group->spawn(downloadFile(...), $file['url'], $downloadDir . '/' . $file['filename']);
}
$results = $group->all();The structured concurrency model (TaskGroup, Scope, Channels) is the genuinely interesting part - and unlike ReactPHP or Amp, there are no special client libraries. It intercepts standard PHP calls transparently, just as Swoole’s hooks do. TrueAsync is explicitly experimental and the API may change. It’s not something I’d run in production today. But the direction is interesting: if this or something like it matures, the async PHP story stops being “pick your incompatible library” and starts being “write normal PHP, run it async.”
I gave it a try with the same SQL test query I used for Swoole and Amp, the results were also good, slightly slower than Amp, but I had to emulate the x86_64 on my MacBook.
Async PHP in 2026 isn’t one thing. Fibers are the primitive. Guzzle gives you concurrent HTTP for free. ReactPHP and Amp give you an event loop for mixed I/O. Swoole and amphp both give you async MySQL - one at the C level, one in pure PHP. And TrueAsync hints at where this is heading.
The “PHP can’t do this” take is about five years out of date. The ecosystem is mature, the patterns are established, and as you’ve seen today, the code isn’t exotic.
Are you using async PHP in production? Which approach did you go with - and would you do it again? Let me know in the comments!
PS. I have developed an AI-powered modernization advisor that includes knowledge from our blog posts to help you plan your dedicated PHP app modernization process. Available for free here, any feedback is more than welcome!
PS2. We help teams modernize PHP applications and fix the architectural problems that hold them back. If that sounds like your situation, we currently have a slot for a new project. Just email me or reach out on LinkedIn.
Why is this newsletter for me?
If you are passionate about well-crafted software products and despise poor software design, this newsletter is for you! With a focus on mature PHP usage, best practices, and effective tools, you'll gain valuable insights and techniques to enhance your PHP projects and keep your skills up to date.
I hope this edition of PHP at Scale is informative and inspiring. I aim to provide the tools and knowledge you need to excel in your PHP development journey. As always, I welcome your feedback and suggestions for future topics. Stay tuned for more insights, tips, and best practices in our upcoming issues.
May thy software be mature!


