PHP engines in 2026: Zend, FrankenPHP, RoadRunner, Swoole, ReactPHP, AmPHP
PHP is no longer one thing in 2026
For twenty years, "PHP in production" meant one thing: Apache or Nginx, PHP-FPM, one process per request, no shared state. That model built the solidity of the ecosystem. It remains the default for 90% of applications.
Since 2020, new approaches have emerged: application servers with persistent workers (RoadRunner, FrankenPHP), async engines with coroutines (Swoole, AmPHP), pure-PHP event loops (ReactPHP). Same language, radically different execution model.
This article presents the six serious options of 2026, their models, benchmarks, pitfalls, and our recommendation per use case.
Zend Engine + PHP-FPM: the historical model
The Zend Engine is the official PHP implementation, maintained by The PHP Foundation. PHP-FPM (FastCGI Process Manager) is the standard process manager for PHP on the web.
"Shared-nothing" model
Each HTTP request runs in a fresh FPM process with a clean symbol table. At the end of the request, everything is destroyed. No shared memory between two requests (except OPcache for bytecode).
Consequences:
- Maximum robustness. Memory leak, exception, corrupted state: the next request starts from zero. No contagion.
- Developer peace of mind. No need to manage the lifecycle of connections, resources, global variables. Everything is recreated.
- Fixed cost per request. Symfony bootstrap (300 classes), DB connection, service init: 30 to 80 ms of overhead paid on every request.
Strengths
- Proven stability over 20 years.
- 100% compatibility with the ecosystem.
- Isolation between requests by design, zero cross-contamination.
- Operationally simple: any host can run it.
Limits
- Impossible to maintain server state (durable WebSocket, long polling, state machine).
- Repeated bootstrap caps maximum throughput.
- No native async: a request blocks its worker for its full duration.
JIT inside the Zend Engine
JIT (PHP 8.0+) dynamically compiles bytecode into machine code. Enabled via opcache.jit, it does not change the execution model, only VM speed. Modest gains on I/O-bound web (5 to 15%), significant on CPU-bound (50 to 300%).
We cover JIT in detail in our OPcache article. In 2026 it is stable and enabled by default on all our engagements.
FrankenPHP: Caddy + embedded PHP
FrankenPHP is a modern HTTP server that embeds PHP directly into a Caddy binary. Developed by Kévin Dunglas (Symfony core team), released in 2022, mature since 2024.
What it brings
- Single binary. One executable, no separate FPM, no reverse proxy to configure.
- Native HTTPS/3. QUIC out of the box via Caddy.
- Worker mode. Optional mode where the application stays loaded in memory between requests. Symfony bootstrap paid once at startup, then each request starts at zero overhead.
- Server-Sent Events, HTTP push, early hints. Native support.
Worker mode
In this mode, FrankenPHP keeps a hot instance of the app in memory. A worker script calls frankenphp_handle_request() in a loop.
<?php
// worker.php
ignore_user_abort(true);
$kernel = require __DIR__ . '/public/index.php';
// Symfony kernel and services instantiated once
for ($nbRequests = 0, $running = true; $running && $nbRequests < 1000;) {
$running = \frankenphp_handle_request(function () use ($kernel) {
// Request handling: kernel::handle
});
$nbRequests++;
gc_collect_cycles();
}
Measured gain on Symfony: 2x to 4x more requests per second compared to PHP-FPM. Median latency divided by 3 (no more bootstrap per request).
Pitfall: shared state
In worker mode, global variables, singletons, DB connections persist between requests. It is both the strength (performance) and the difficulty (every state bug becomes global).
- DB connection: use a pool. Beware of MySQL session resets, orphan transactions.
- In-memory cache: fine, but mind the per-worker limit.
- Open files, sockets: always close explicitly.
- Symfony: the
Runtimestack handles state, but some bundles are not worker-safe.
Our usage
FrankenPHP is rising quickly in our engagements. We deploy it systematically on high-load APIs and new Symfony architectures. On legacy, PHP-FPM remains safer.
RoadRunner: Go app server + PHP workers
RoadRunner (SpiralScout) is a Go-written app server that orchestrates persistent PHP workers via pipes. Older than FrankenPHP, more mature on some operational aspects.
Architecture
┌────────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ RoadRunner│────▶│ PHP worker│
│ (HTTP, │ │ (Go, │ │ (long- │
│ gRPC, …) │ │ orchestr.)│ │ lived) │
└────────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ PHP worker│
│ (long- │
│ lived) │
└──────────┘
PHP workers stay in memory and consume jobs. RoadRunner handles recycling (max memory, max jobs) to avoid leaks.
Strengths
- Rich plugins. Temporal, Centrifugo, gRPC, Kafka, AMQP, Memcached: native Go connectors.
- Automatic recycling. Protection against memory leaks that destabilize workers.
- Excellent Symfony and Laravel integration. Official bundles and packages maintained.
- Production-grade. Used by Badoo, Spiral, several large-scale operators.
Symfony configuration example
# .rr.yaml
rpc:
listen: tcp://127.0.0.1:6001
server:
command: "php public/index.php"
http:
address: :8080
middleware: ["gzip", "headers"]
pool:
num_workers: 8
max_jobs: 1000
allocate_timeout: 60s
destroy_timeout: 60s
reload:
enabled: true
interval: 1s
patterns: [".php"]
services:
http:
dirs: ["./src", "./config"]
Our recommendation: RoadRunner on projects needing more than a plain HTTP server (queues, gRPC, WebSocket via Centrifugo). FrankenPHP on pure-HTTP projects.
Swoole and OpenSwoole: async + coroutines
Swoole is a PHP extension written in C that adds an event loop, coroutines and async programming. OpenSwoole is a newer community fork that took over maintenance.
What it changes
- A worker can handle several concurrent requests thanks to coroutines. Non-blocking I/O: while waiting for Redis, another request is served.
- Radically different mental model. Closer to Go or Node.js than classic PHP.
- Highest raw performance: 30,000 to 100,000 req/sec on a medium server.
Example
use Swoole\Http\Server;
$server = new Server('0.0.0.0', 9501);
$server->on('request', function ($request, $response) {
// Non-blocking Redis call via coroutine
$redis = new \Swoole\Coroutine\Redis();
$redis->connect('redis', 6379);
$value = $redis->get("cache:{$request->server['request_uri']}");
$response->header('Content-Type', 'application/json');
$response->end(json_encode(['value' => $value]));
});
$server->start();
Limits
- Changes everything. Porting classic Symfony code requires adaptation. Many extensions and bundles are not coroutine-safe.
- Harder debugging. Stack traces across coroutines are less readable.
- Smaller ecosystem. Outside China, where Swoole dominates, community support is thinner.
Our usage
Rare in Europe. Relevant for extreme SaaS APIs at very high load. Most projects have more to gain by moving to FrankenPHP worker mode, without changing the mental model.
ReactPHP: pure PHP async
ReactPHP implements an event loop in pure PHP, no extension needed. Node.js inspiration, promises and streams.
use React\Http\HttpServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;
$server = new HttpServer(function (ServerRequestInterface $request) {
return new Response(200, ['Content-Type' => 'text/plain'], "Hello");
});
$socket = new React\Socket\SocketServer('0.0.0.0:8080');
$server->listen($socket);
Nicher than the others, very good for long-running agents, async workers, WebSocket servers. Weaker for classic HTTP APIs (FrankenPHP does better).
AmPHP: non-blocking coroutines with Fibers
AmPHP (async multi-tasking PHP) leverages native PHP 8.1+ Fibers to offer structured concurrency. Very elegant for complex async tasks (orchestrating 10 parallel API calls, for example).
use Amp\Future;
// Parallelize 3 API calls
[$user, $orders, $recommendations] = Future\await([
Future::complete($userClient->fetch($id)),
Future::complete($ordersClient->fetchAll($id)),
Future::complete($recoClient->compute($id)),
]);
Particularly useful in Symfony Messenger: an async worker can process multiple concurrent messages with a single PHP process.
Comparison table
| Engine | Model | Relative perf | Maturity | Ecosystem | Learning curve |
|---|---|---|---|---|---|
| Zend + PHP-FPM | Shared-nothing | 1x | 20 years | Full | Zero |
| FrankenPHP worker mode | Persistent worker | 3 to 5x | Mature 2024+ | Native Symfony | Low |
| RoadRunner | Go app server + workers | 3 to 5x | Mature 2020+ | Symfony + Laravel | Medium |
| Swoole / OpenSwoole | Async + coroutines | 5 to 15x | Mature but Europe-niche | Limited | High |
| ReactPHP | Pure PHP event loop | 5 to 10x | Mature, niche | Limited | Medium |
| AmPHP (with Fibers) | Non-blocking coroutines | 3 to 8x | Maturity 2023+ | Limited | Medium |
Quick benchmark
Internal benchmark on a "hello world" JSON endpoint (k6, 100 constant VUs, 60 s, 4 vCPU 8 GB server).
| Engine | Requests/sec | p95 latency | RSS memory |
|---|---|---|---|
| PHP-FPM 8.3 + Nginx | 4,200 | 35 ms | 280 MB |
| FrankenPHP worker mode | 14,500 | 12 ms | 180 MB |
| RoadRunner | 13,800 | 14 ms | 210 MB |
| Swoole | 38,000 | 4 ms | 120 MB |
| ReactPHP | 11,000 | 18 ms | 140 MB |
On a full-stack Symfony endpoint (routing + DI + Doctrine query + JSON render), the gaps shrink: bootstrap is no longer the bottleneck, business logic is.
| Engine | Requests/sec | p95 latency |
|---|---|---|
| PHP-FPM 8.3 | 1,200 | 85 ms |
| FrankenPHP worker mode | 3,400 | 38 ms |
| RoadRunner | 3,100 | 42 ms |
| Swoole | 4,800 | 28 ms |
FrankenPHP captures 80% of the theoretical gain with 10% of the adoption effort.
Recommendations per use case
Summary of what we recommend on our engagements.
- Classic CRUD app, SMB, internal B2B → PHP-FPM + Zend. Do not change. Operational complexity of other options does not pay off.
- High-load stateless API → FrankenPHP worker mode. Best trade-off in 2026.
- Real-time, WebSockets, long polling → Swoole or RoadRunner + Centrifugo. Shared-nothing cannot hold durable state.
- Light microservices, RPC, gRPC → RoadRunner. Rich plugins, solid orchestration.
- Complex async tasks, API orchestration → AmPHP + Symfony Messenger. Fiber-based structured concurrency is elegant.
- Async PHP workers on Messenger → AmPHP for concurrency, RoadRunner for raw throughput.
- Mixed stack, need for native HTTP/3 → FrankenPHP. The only one with Caddy + QUIC out of the box.
Recurring migration pitfalls
Moving from PHP-FPM to worker mode (FrankenPHP, RoadRunner, Swoole) exposes bugs the shared-nothing model hid.
- Global variables.
$_SESSIONor a global variable corrupts subsequent requests. Always clean up indestruct. - Exhausted DB connections. Symfony reuses the connection, but the Doctrine pool may saturate. Size
max_connectionsaccordingly. - Memory leaks. A bug tolerable in FPM (cleaned per request) becomes catastrophic in worker mode (memory climbs until kill). Strict monitoring and
max_jobsfor recycling. - Non-worker-safe Symfony bundles. Some bundles store global state (rare in 2026, but check).
- Logging and file handlers. A
file_put_contents('logs.txt', $msg, FILE_APPEND)held across requests blocks everything. Use Monolog with async handlers. - Orphan transactions. A
beginTransactionwithoutcommitorrollbacklingers. Add middleware that always closes out.
Conclusion
Choosing a PHP engine in 2026 is no longer obvious. PHP-FPM remains the default answer for 80% of cases: reliable, known, predictable. For the remaining 20% (high-load APIs, real-time, complex async orchestration), FrankenPHP, RoadRunner, Swoole and AmPHP open ground PHP could not cover five years ago.
Our practical rule: only switch engine when the profiler shows bootstrap dominates, or when a functional need (WebSocket, complex async) makes PHP-FPM structurally unfit. Measure, compare, do not follow fashion.
For PHP architecture scoping, a benchmark on your existing stack or a migration to worker mode, reach out at contact@your-digital-hub.com or explore our PHP expertise and our software architecture service.