FrankenPHP + Drupal: Great Server, but Skip Worker Mode
I have been quietly moving side projects onto FrankenPHP, and it is genuinely one of the nicest things to happen to the PHP hosting story in years. But there is a persistent myth I want to kill in this post: no, you cannot get Swoole-level, keep-it-in-memory throughput out of Drupal on FrankenPHP yet. You can still run Drupal on it happily today — just in classic mode. Here are my honest field notes.
What FrankenPHP actually is
FrankenPHP is a modern PHP application server built on top of the Caddy web server and written in Go (with a bit of C to embed the PHP interpreter). In practice that buys you a lot out of the box:
- Automatic HTTPS via Caddy, plus HTTP/2 and HTTP/3 with zero extra config.
- A single self-contained binary (or the
dunglas/frankenphpDocker image) — no separate FPM pool and nginx to wire together. - It now lives under the PHP Foundation at the
php/frankenphprepo, so it is no longer just a one-person side quest.
Even without any of the fancy bits, it is a perfectly good FPM replacement.
Worker mode: the part everyone quotes
The headline feature is worker mode. Instead of booting your framework on every request, FrankenPHP boots the app once, keeps it resident in memory, and loops over incoming requests with frankenphp_handle_request(). Superglobals get reset between requests; your bootstrap does not run again. Reports suggest large throughput gains (Dunglas has cited figures like ~3.5x over FPM on API Platform) — but run your own benchmark before you quote a number.
Worker mode is the difference between starting your car at every stop sign and just leaving the engine running. Fantastic — if your app was designed to keep the engine running.
That "if" is the whole story. Long-lived processes change the rules, and the gotchas are real:
- State bleed: static properties, globals and singletons persist across requests. You must reset request-specific state or you leak one user's data into another's response.
- Memory leaks accumulate because the process never dies — you cap it with
MAX_REQUESTSso workers recycle. - $_ENV is not reset between requests, unlike the other superglobals.
- Sessions and PHP Fibers have known rough edges in worker mode.
Laravel Octane and Symfony (natively since 7.4, via the FrankenPHP runtime before that) already handle this cleanup for you through their reset mechanisms. That is the key detail.
The Drupal asterisk (Symfony Runtime)
Here is the blocker: Drupal cannot use worker mode yet because it does not support Symfony Runtime. Drupal goes straight from index.php into DrupalKernel, so there is no runtime layer to own the app lifecycle and reset services (implementing ResetInterface) between requests. The tracking work lives in Drupal core issue #3313404 and the frankenphp-drupal repo's issue #1 — both conclude the path forward is patching Drupal onto Symfony Runtime first.
On top of that, Drupal's BigPipe leans on streaming, and FrankenPHP has documented crashes when certain constructs run inside Fibers — so the Fibers/BigPipe combination is its own separate concern. Until both are sorted, treat Drupal worker mode as "not yet."
My setup: classic mode behind Traefik on Coolify
The good news: classic mode works fine, and that is what I run. My box is an OVH VPS running Coolify, with Traefik terminating TLS out front, so I let FrankenPHP just serve plain HTTP internally and skip its own certificate handling. A minimal image looks like this:
FROM dunglas/frankenphp
COPY . /app
# Classic mode: Drupal just works, no worker directive.
# Traefik terminates TLS, so serve plain HTTP on 80 internally.
CMD ["frankenphp", "php-server", "--root", "/app/web", "--listen", ":80"]
If this were a Symfony or Laravel app, this is the one line I would add to turn the engine on — and deliberately do not for Drupal:
{
frankenphp {
worker ./public/index.php 4
}
}
If you want the origin story straight from the creator, Kévin Dunglas's talk is worth 40 minutes:
Bottom line: adopt FrankenPHP for Drupal today for the clean single-binary deploy and free HTTP/3 — just set your expectations in classic mode, and keep worker mode in your pocket for the Symfony and Laravel projects that can actually use it.