The default PHP deployment — PHP-FPM behind Nginx, one request per process, the process reset or reused for the next request — is a genuinely good model, and it's why PHP has stayed boring and reliable for two decades. It's also not the only way to run PHP anymore. ReactPHP, Amphp (built on the Revolt event loop), and Swoole all let a single PHP process stay alive and handle many things concurrently, and each takes a real tradeoff to get there that the request-per-process model doesn't force on you.
Why request-per-process is the default for good reason
Every PHP-FPM request starts with a clean slate: no global state survives from the previous request, a bug that corrupts memory or leaves an object in a bad state dies with the process, and a crash in one request can't take down another request being served concurrently. That isolation is worth a lot, and it's the reason most PHP applications never think about "runtime" as a concept at all — they just write code and the process model handles the concurrency and cleanup for them.
Event loops: ReactPHP and Amphp/Revolt
ReactPHP and Amphp both run a single-threaded event loop — one process, cooperative multitasking, no operating system threads — where a "blocking" call like a database query or HTTP request is replaced with a non-blocking equivalent that registers a callback (or, since PHP 8.1, suspends a Fiber) and lets the loop move on to other work while waiting. Amphp's newer versions build directly on Fibers via Revolt, which is what gives Amphp 3.x code the look of ordinary synchronous PHP even though it's running cooperatively.
use function Amp\async;
use function Amp\Future\awaitAll;
// Both HTTP calls happen concurrently on one thread, one process —
// the event loop switches between them while each waits on I/O
[$errors, $responses] = awaitAll([
async(fn() => file_get_contents('https://api.example.com/rates')),
async(fn() => file_get_contents('https://api.example.com/inventory')),
]);
In an event-loop runtime, everything shares one thread. A single call to a blocking function — a synchronous curl_exec or a raw PDO query not routed through the loop's async driver — blocks every other pending request in that process until it returns. This is the sharpest footgun in event-loop PHP: libraries that look async-compatible but quietly call a blocking function internally will silently degrade concurrency without an obvious error.
Swoole and OpenSwoole: coroutines with less ceremony
Swoole (and its community fork OpenSwoole) take a different approach: a C extension that replaces PHP-FPM entirely, running an HTTP server inside long-lived PHP worker processes and using coroutines to handle concurrency with far less code restructuring than event-loop libraries need — Swoole can transparently "hook" standard blocking calls like mysqli or file I/O so they become non-blocking coroutines without you rewriting the call sites. That convenience is also the risk: it changes the runtime semantics of code that looks unmodified, so behavior under Swoole can diverge from behavior under plain PHP-FPM in ways that are easy to miss until load testing.
The tradeoff nobody skips: shared process state
The entire value proposition of these runtimes — keeping a process alive across many requests — is also their central risk. Global state, static properties, singletons, and anything cached in a long-lived object now persists between requests instead of resetting, which means a bug that leaks memory, holds a stale database connection, or accumulates state in a static array will compound across thousands of requests instead of dying with the process. Laravel's framework internals in particular assume a single request lifecycle per process; running a Laravel app under Swoole or a long-running event loop (via packages like Laravel Octane) requires actively resetting container bindings and service state between requests, not just leaving the framework running as-is.
If your app's bottleneck is a slow database query or an N+1 problem, an async runtime won't fix it — it will just let more requests wait concurrently instead of sequentially, which can mask the underlying problem while adding real operational complexity. Reach for one of these when you specifically need long-lived connections (WebSockets), very high connection concurrency (thousands of simultaneous slow clients), or a background worker doing many concurrent outbound calls — not as a default performance upgrade.
Wrapping up
ReactPHP, Amphp/Revolt, and Swoole all solve the same underlying problem — letting one PHP process do many things concurrently instead of one thing per process — but they get there differently and they all trade away the request isolation that makes PHP-FPM forgiving. That tradeoff is worth it for WebSocket servers, high-concurrency proxies, and queue workers making many outbound calls at once. It's not worth it for a typical Laravel CRUD app whose real bottleneck is a database index, and adopting one of these runtimes without a specific concurrency problem to solve usually just trades a simple, well-understood deployment model for a harder-to-debug one.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.