Chat on WhatsApp
Performance 11 min read

FrankenPHP Worker Mode: Near-Go PHP Throughput (and What It Means for Magento)

FrankenPHP worker mode boots your framework once and reuses it across requests for roughly 3–4× the throughput of nginx + PHP-FPM. Here is how it works, where it shines, and the honest take for Magento.

FrankenPHP Worker Mode: Near-Go PHP Throughput (and What It Means for Magento)

Most PHP developers are still running the same stack they ran in 2015: nginx in front, PHP-FPM behind, a fresh framework boot on every single request. FrankenPHP worker mode is the easy performance win that quietly fixes that, and it's the kind of upgrade you can ship in an afternoon. This is your no-fluff guide to PHP worker mode, the worker loop, and where it does (and doesn't) fit Magento.

throughput in worker mode
15000req/sec reported (worker)
1binary replaces 2 services
2workers per CPU by default

What FrankenPHP actually is

FrankenPHP is a modern PHP runtime and application server built on top of the Caddy web server, which is itself written in Go. Instead of wiring up nginx to talk FastCGI to a pool of PHP-FPM processes, you ship one static binary that is both the web server and the PHP runtime. That single binary handles TLS termination, request routing, and PHP execution in one process tree.

Because it inherits Caddy's networking stack, you get a lot of modern web plumbing without configuring anything: native HTTP/2, native HTTP/3, automatic HTTPS with certificate management, 103 Early Hints to start preloading assets before your response body is ready, and built-in Mercure for real-time server-sent events. In 2026 the project also gained official PHP Foundation backing, which matters if you care about long-term stewardship before you bet production traffic on it.

Key point

FrankenPHP is not a PHP-FPM tweak or an OPcache flag. It's a different way to serve PHP entirely: one Go-powered binary instead of two cooperating services, with worker mode as the headline feature.

FrankenPHP vs PHP-FPM in one sentence

The short version of FrankenPHP vs PHP-FPM: PHP-FPM forks a pool of short-lived workers that each rebuild your application from scratch per request, while FrankenPHP can keep your application alive in memory and feed requests straight into it. That difference is the whole reason people start chasing "make PHP as fast as Go" benchmarks.

Why worker mode is the unlock

Here's the thing classic PHP hides from you: every request pays a tax. Autoloading the framework, parsing config, building the dependency-injection container, wiring up the router, opening connections. On a heavy framework that bootstrap can dominate the request, especially for small JSON API responses where the actual business logic is trivial.

PHP worker mode deletes that tax. The application, your Laravel, Symfony, or Slim kernel, boots once into memory when the worker starts. After that, each incoming HTTP request reuses the already-booted app object. Framework boot overhead per request drops to near zero, and the only work left is the request itself.

This is why reported benchmarks land around 15,000 req/sec in worker mode against roughly 4,000 req/sec for traditional PHP-FPM on the same workload: a 3–4× jump without touching your business logic. You're not making your code faster; you're refusing to throw away the expensive setup work after every request.

Classic PHP rebuilds your entire app on every request and then sets it on fire. Worker mode keeps it warm.
Key point

The win scales with how heavy your boot is. A thin Slim API sees a modest bump; a fat framework with a big DI container and lots of service providers sees the biggest gains, because boot was eating the most.

The worker loop, line by line

If you're on a framework, you'll use its integration (more on that below). But understanding the raw loop is the fastest way to grasp what worker mode is actually doing. Here's the vanilla-PHP worker script, the heart of any FrankenPHP worker mode tutorial:

require __DIR__.'/vendor/autoload.php';
$myApp = new \Panth\App\Kernel();
$myApp->boot();
$handler = static function () use ($myApp) {
    echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nb = 0; !$maxRequests || $nb < $maxRequests; ++$nb) {
    $keepRunning = \frankenphp_handle_request($handler);
    $myApp->terminate();
    gc_collect_cycles();
    if (!$keepRunning) break;
}
$myApp->shutdown();

Read it top to bottom. The require and the new Kernel() plus boot() run once, before the loop: that's your expensive setup, paid a single time per worker. The $handler closure captures the booted app so each request can call into it.

Inside the loop, frankenphp_handle_request($handler) blocks until a request arrives, populates the superglobals ($_GET, $_POST, and friends), runs your handler to emit the response, and returns whether the worker should keep going. After each request you call $myApp->terminate() for post-response cleanup, then gc_collect_cycles() to force PHP's cyclic garbage collector to run: critical in a long-lived process where leaks accumulate. The $maxRequests guard lets the worker exit cleanly after N requests so a fresh process can replace it.

Key point

The mental model: everything above the loop is shared across thousands of requests. Everything inside the loop runs per request. Bugs in worker mode almost always come from state leaking from the per-request zone into the shared zone.

Running it: workers, threads, and memory leaks

You configure FrankenPHP through a Caddyfile or environment variables. The most direct way to declare a worker script and how many copies to run is the FRANKENPHP_CONFIG env var:

# run public/index.php as a worker, 42 instances
export FRANKENPHP_CONFIG="worker ./public/index.php 42"
frankenphp run

By default FrankenPHP spins up two workers per CPU core, which is a sane starting point. Override it when you've measured your own workload: I/O-bound apps often want more workers than CPU-bound ones. Containerizing it is genuinely tiny:

FROM dunglas/frankenphp

COPY . /app/public

# optional: enable worker mode for the whole image
ENV FRANKENPHP_CONFIG="worker ./public/index.php"

The thread-safety catch (ZTS)

Worker mode requires a thread-safe build of PHP, known as ZTS: Zend Thread Safety. That's not optional: workers run in threads, so the runtime has to be thread-safe. The practical consequence is that some C extensions were never written to be ZTS-compatible, so they won't load in worker mode. Audit your extension list before you commit. Most mainstream extensions are fine; the long tail of niche ones is where you'll get surprised.

Memory leaks are real: plan for them

In a classic PHP-FPM request, the process dies at the end and the OS reclaims everything, so leaks are invisible. In a long-lived worker, every leaked byte sticks around. Many libraries and a lot of legacy code leak memory across requests: static caches that never clear, event listeners that pile up, references that outlive their request. Two mitigations carry most of the load:

First, restart each worker after a fixed number of requests using the MAX_REQUESTS env var (the $maxRequests guard in the loop above). A leaky worker that recycles every few thousand requests never grows unbounded. Second, call gc_collect_cycles() after each request to force collection of reference cycles that PHP's normal refcounting won't free on its own. Neither is a license to write leaky code, but together they make real-world apps stable in worker mode.

Treat a worker like a server process, not a script. It boots, it serves, it recycles, and it will happily hold onto your bugs for hours if you let it.

Laravel and Symfony get it almost for free

You will probably never write that raw loop by hand. The big frameworks already ship worker integrations. For Laravel, FrankenPHP is an official server for Laravel Octane, so the Laravel Octane FrankenPHP setup is three commands:

composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:start --server=frankenphp

Octane handles the boot-once-reuse-many lifecycle, resets framework state between requests, and gives you hooks to clear anything stateful in your own code. Symfony has an official FrankenPHP runtime that does the equivalent, swapping the standard runtime for one that drives the worker loop. In both cases you're using a battle-tested integration that already knows where the framework hides mutable state, instead of debugging it yourself.

If you build companion microservices around an e-commerce stack, a webhook receiver, a headless catalog API, a queue-worker dashboard, those are exactly the small, stateless, high-throughput services where worker mode pays off fastest. That's also where I'd reach for Magento extension development patterns to keep the integration clean.

Where this leaves Magento

Time for the honest FrankenPHP for Magento take, because this is where a lot of breathless blog posts overclaim. Magento 2 is not worker-safe out of the box. Its dependency-injection container and ObjectManager accumulate stateful singletons over the course of a request, so naive worker mode would leak state from one customer's request into the next. That's not a tuning problem you fix with an env var: it's an architectural assumption baked into the framework. Treat full Magento worker mode as experimental and unsupported today.

But don't write FrankenPHP off for Magento, because classic (non-worker) mode is still a legitimate win. You collapse nginx + PHP-FPM into one binary, you get native HTTP/3 and 103 Early Hints (great for pushing critical CSS hints sooner), automatic HTTPS, and dramatically simpler Docker images. None of that requires worker mode, and none of it touches Magento's state model. It pairs nicely with the rest of a Magento performance optimization program.

Key point

If you also build Laravel or Symfony side projects, or Magento companion microservices, worker mode is the easy 3–4× win today. For the Magento monolith itself, run FrankenPHP in classic mode and bank the HTTP/3, Early Hints, and ops simplification.

FrankenPHP is one of the clearest examples of a PHP application server 2026 story: the runtime got faster, the deployment got simpler, and the only thing standing between you and a 3–4× API is a framework that already supports it. For most teams the hardest part is admitting they could have done it last year.

Frequently asked questions

Do I have to use worker mode to use FrankenPHP?

No. FrankenPHP runs perfectly well in classic, non-worker mode, behaving like a drop-in replacement for nginx + PHP-FPM. You still get HTTP/3, automatic HTTPS, and 103 Early Hints. Worker mode is an opt-in feature you enable when your framework and code are ready for it.

How much faster is FrankenPHP worker mode really?

Reported benchmarks show roughly 15,000 req/sec in worker mode versus about 4,000 req/sec for traditional PHP-FPM on the same workload, so call it 3–4×. The exact number depends on how heavy your framework boot is: the heavier the boot, the bigger the win, because you stop repeating it on every request.

Why does worker mode need thread-safe PHP?

Workers run in threads inside the FrankenPHP process, so the PHP runtime must be a thread-safe (ZTS) build. The catch is that some C extensions were never made ZTS-compatible and won't load in worker mode. Audit your extension list before switching a production app over.

How do I deal with memory leaks in a long-lived worker?

Two standard mitigations: set MAX_REQUESTS so each worker restarts after a fixed number of requests, and call gc_collect_cycles() after every request to clean up reference cycles. Together they keep leaky libraries and legacy code from growing memory without bound, even though they don't fix the underlying leaks.

Can I run Magento 2 in FrankenPHP worker mode?

Not safely today. Magento's DI container and ObjectManager accumulate stateful singletons across a request, so naive worker mode would leak state between requests. Run Magento in FrankenPHP's classic mode instead: you still get HTTP/3, Early Hints, automatic HTTPS, and a simpler one-binary deployment.

Do I need to write the worker loop myself for Laravel or Symfony?

No. Laravel uses Octane (FrankenPHP is an official Octane server) and Symfony has an official FrankenPHP runtime. Both manage the boot-once-reuse-many lifecycle and reset framework state for you, so you install the integration and start the server rather than hand-rolling frankenphp_handle_request().

Need your Magento or PHP stack faster? I'm an Adobe-certified Magento & Hyvä developer and I tune PHP performance for a living.

Hire me