Languages & Runtimes
FrankenPHP: a PHP application server with the interpreter inside Caddy
php/frankenphp on GitHub
By Mira Vance
Published 22 October 2026 · Last reviewed 22 October 2026

FrankenPHP runs every request on a thread of one process: the PHP interpreter is compiled into the Caddy binary through cgo, so a single request that allocates past PHP's hard memory_limit exits the process and takes every in-flight request with it, which is why the docs tell you to run it under a supervisor.
Drafted with AI assistance, researched, verified and edited by the HastCode editorial team. We publish only as many pieces as the Author can verify this way.
TL;DR
• FrankenPHP is an application server for PHP built on the Caddy web server: one binary serves HTTP/1.1, HTTP/2 and HTTP/3 with automatic HTTPS and replaces Nginx or Apache plus PHP-FPM.
• The design choice: the interpreter is embedded in Go through cgo, and each PHP execution runs on a real POSIX thread of the same process, which is why it needs a thread-safe (ZTS) PHP build.
• Worker mode boots your application once and loops on frankenphp_handle_request(); Laravel, Symfony and Yii enable it without application-code changes.
• Since June 2025 the repository lives in the php GitHub organisation, with the PHP Foundation contributing to its development.
• Adopt it for Laravel or Symfony apps you deploy as containers or single binaries. Wait if you depend on imap, newrelic or pcov, which are not thread-safe, or on legacy code you cannot audit for state that leaks between requests.
In 60 seconds
The first command is the README's install script for Linux and macOS, which installs a static binary with PHP 8.5 and most popular extensions built in; the second, from the Usage section, serves the current directory. Docker images are the other documented route.
Architecture
Walk the diagram left to right. Caddy accepts the connection and handles TLS, HTTP/2 and HTTP/3 (README). The FrankenPHP Caddy module, configured by the php_server directive, resolves the script path (classic mode) or picks the worker (worker mode), wraps the request in a frankenPHPContext and sends it down a Go channel to a PHP thread (internals).
Those threads are the heart of the design. The interpreter is embedded through cgo, and each PHP execution runs on a real POSIX thread rather than a goroutine, because PHP's Zend Thread Safety model requires it: Go owns the pool, routing and auto-scaling, C owns the SAPI lifecycle and script loop, and a state machine in internal/state/ keeps them in step (internals). Hence PHP must be built with --enable-zts and --enable-embed.
A regular thread runs one script per request: beforeScriptExecution() returns the filename, C executes it, and Go callbacks such as go_ub_write and go_write_headers carry output and headers back to Caddy. A worker thread keeps the script alive: frankenphp_handle_request() blocks in waitForWorkerRequest() until Go hands it a request, your handler runs, and go_frankenphp_finish_worker_request() cleans up (internals).
State lives in each thread: Zend's allocator frees PHP memory at request shutdown, $_SERVER is rebuilt per request, and $_ENV is imported once per worker start. num_threads threads serve non-worker requests, worker threads are added on top, and max_threads caps the total; a request that waits over 5 ms while CPU is under 80% boots an extra thread, and idle extras retire after max_idle_time (config, internals).
Signals
Use it if
• You run Laravel, Symfony or Yii: all three enable worker mode without application-code changes. Symfony 7.4+ supports it natively, and Laravel drives it through Octane with php artisan octane:install --server=frankenphp.
• You want one container instead of two: the migration guide maps pm.max_children to num_threads, your php.ini works as-is, and Nginx, PHP-FPM, FastCGI config and self-managed TLS certificates all go.
• You ship PHP as a single executable: EMBED=dist/app/ ./build-static.sh bundles your app, the interpreter and Caddy into one static binary.
• You write Go and want PHP behind a net/http handler: frankenphp.NewServer() returns an http.Handler.
Don't use it if
• Your app needs imap, newrelic or pcov: they are not thread-safe and are unsupported. Stay on PHP-FPM, where each worker is its own process.
• You need per-request process isolation. Every request runs in one process, so a request past the hard memory limit stops the server; PHP-FPM loses one child instead.
• Your legacy code leaks memory or relies on globals being reset: worker mode keeps statics, ini_set() and $_ENV across requests. Run classic mode, which keeps the single binary, or set MAX_REQUESTS to recycle workers.
Alternatives
• PHP-FPM: the FastCGI process manager that ships with PHP, behind Nginx or Apache; a process per worker, no worker mode, and still fully supported alongside FrankenPHP.
• RoadRunner: also a Go server with a long-running PHP mode, but it supervises separate PHP worker processes instead of embedding the interpreter, so a standard non-ZTS PHP build works.
• Swoole: a C extension that puts an event loop and coroutines inside PHP itself; a different programming model, with no web server in front.
• NGINX Unit: a polyglot application server that loads PHP as one language module among several, in the classic one-run-per-request model.
Changelog
• v1.13.2 21 October 2026
• v1.13.1 16 October 2026
• v1.13.0 4 October 2026
• v1.12.7 7 August 2026
• v1.12.6 21 July 2026
• v1.12.5 20 July 2026
• v1.12.4 4 June 2026
• v1.12.3 15 May 2026
• v1.12.2 9 April 2026
• v1.12.1 10 March 2026
FAQ
Is FrankenPHP production ready?
Yes for mainstream frameworks: FrankenPHP is on a 1.x line with patch releases every few weeks (see Signals), and Laravel, Symfony and Yii integrate it officially. Run it under a supervisor: exceeding PHP's hard memory limit exits the whole process.
What is the difference between FrankenPHP worker mode and classic mode?
Classic mode runs a PHP file per request, like PHP-FPM. Worker mode boots the app once and loops on frankenphp_handle_request(), so statics, globals and ini_set() persist between requests.
How does FrankenPHP compare to PHP-FPM?
FrankenPHP serves requests from threads of one Go process that also terminates HTTP; PHP-FPM forks processes behind a separate web server. The migration guide maps pm.max_children to num_threads and pm = dynamic to max_threads auto.
Which PHP extensions do not work with FrankenPHP?
FrankenPHP's known issues list imap, newrelic and pcov as unsupported (not thread-safe), and imagick as crash-prone in the Docker and Homebrew builds unless ImageMagick's OpenMP threading is disabled.
What are the alternatives to FrankenPHP?
The main alternatives to FrankenPHP are PHP-FPM behind Nginx or Apache (a process per worker), RoadRunner (a Go server supervising PHP processes), Swoole (an event loop inside PHP) and NGINX Unit (a polyglot application server).
How is FrankenPHP licensed and who maintains it?
FrankenPHP is MIT licensed; its optional Mercure library is AGPL. Kévin Dunglas created it, Les-Tilleuls.coop sponsors it, and since 2025 it lives in the php GitHub organisation, the PHP Foundation contributing and governance unchanged.
Verdict
FrankenPHP is careful systems engineering: a Go host that compiles the PHP interpreter in, fronts it with Caddy's HTTPS and HTTP/3, and keeps your application booted between requests. We recommend it for Laravel and Symfony teams shipping containers or single binaries, especially where two containers would become one. We would hold back where process isolation matters more than footprint, where an unsupported extension such as imap or newrelic is load-bearing, or where legacy code cannot be audited for state that leaks across worker requests; PHP-FPM stays fully supported for exactly those cases. Thread-safe builds of the unsupported extensions, or per-request isolation inside the process, would widen that recommendation.
Sources
• FrankenPHP internals: architecture overview
• FrankenPHP docs: worker mode
• FrankenPHP docs: classic mode
• FrankenPHP docs: configuration
• FrankenPHP docs: known issues
• FrankenPHP docs: migrating from Nginx and PHP-FPM
• FrankenPHP docs: compile from sources
• FrankenPHP docs: standalone binaries
• FrankenPHP docs: use as a Go library
• The PHP Foundation: FrankenPHP Is Now Officially Supported by The PHP Foundation (15 May 2025)
• The PHP Foundation: 30 years of PHP, FrankenPHP is now part of the PHP organisation (8 June 2025)
• https://github.com/php/frankenphp
• https://github.com/php/frankenphp/releases
• https://github.com/php/frankenphp/graphs/contributors
• https://github.com/php/frankenphp/tree/main/.github
• https://github.com/php/frankenphp/blob/main/README.md
Get new Explainers by email
How real systems are built, in your inbox. No spam; unsubscribe any time.
More in Languages & Runtimes
1. ConceptStructured concurrency: why child tasks never outlive their parentStructured concurrency ties concurrent tasks to the block that starts them, so none outlives its parent and errors and cancellation follow the tree.26 October 2026
Related
1. ComparisonConnection reuse in HTTP/1.1, HTTP/2 and HTTP/3: how each works and where each breaksHow HTTP/1.1, HTTP/2 and HTTP/3 reuse connections, where each one breaks, and which to use for calls between services.26 October 2026
2. Repo Profileetcd: a Raft-replicated key-value store for coordination dataetcd is a Raft-replicated, linearizable key-value store for coordination-sized data; fast disks and a few-gigabyte ceiling are the price.22 October 2026
3. Repo Profilewitr: a CLI that traces a process, port or container to what started itwitr is a single-binary CLI and TUI that traces a process, port, container or file to what started it: a point-in-time diagnostic, not a monitor.22 October 2026


