FrankenPHP & Modern PHP Runtimes: A Deep Dive into High-Performance PHP
Explore FrankenPHP, a modern PHP runtime built on Caddy and Go.
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
- ✓PHP 8.1 or higher
- ✓Basic knowledge of PHP and web servers
- ✓Familiarity with Caddy or Nginx configuration
- FrankenPHP is a modern PHP application server built on top of Caddy and Go.
- It supports worker mode for persistent scripts, eliminating cold starts.
- Built-in support for early hints, HTTP/2, HTTP/3, and automatic HTTPS.
- Can handle concurrent requests with a single PHP-FPM process.
- Integrates seamlessly with Symfony, Laravel, and other frameworks.
Think of PHP as a chef who prepares a meal from scratch every time a customer orders. FrankenPHP is like a sous chef who stays in the kitchen, preps ingredients ahead, and can handle multiple orders at once without restarting. This makes the restaurant (your web app) much faster and more efficient.
PHP has long been the workhorse of the web, powering millions of websites. However, traditional PHP runtimes like FPM have inherent limitations: each request starts a new process, loads the entire application, and then tears it down. This leads to cold starts and wasted resources. Enter FrankenPHP, a modern PHP application server that reimagines how PHP runs. Built on top of Caddy (a Go-based web server) and the Go library for PHP, FrankenPHP introduces a worker mode that keeps your application in memory across requests. It also supports early hints, HTTP/2 and HTTP/3, automatic HTTPS, and concurrent request handling within a single PHP process. This tutorial will guide you through setting up FrankenPHP, understanding its architecture, and leveraging its features to build high-performance PHP applications. Whether you're running a Laravel API, a Symfony site, or a custom PHP app, FrankenPHP can dramatically reduce latency and improve throughput. We'll cover installation, configuration, worker mode, debugging, and real-world production considerations. By the end, you'll have a solid grasp of how to deploy PHP applications with modern runtimes for maximum performance.
What is FrankenPHP?
FrankenPHP is a modern PHP application server written in Go that integrates with Caddy. It replaces traditional PHP-FPM with a more efficient model. Key features include: worker mode (persistent scripts), early hints (103 status code), support for HTTP/2 and HTTP/3, automatic HTTPS via Let's Encrypt, and the ability to handle concurrent requests within a single PHP process. It's designed to be a drop-in replacement for PHP-FPM, compatible with existing PHP applications. FrankenPHP can also be used as a standalone server or as a Caddy module. Its architecture leverages Go's concurrency model to handle many requests with low overhead.
Installation and Setup
Installing FrankenPHP is straightforward. You can download a prebuilt binary from the official GitHub releases or compile from source. For this tutorial, we'll use the binary. First, download the appropriate version for your OS. Then, make it executable and optionally move it to your PATH. Create a simple Caddyfile to configure FrankenPHP. Here's a minimal example:
``` { frankenphp }
localhost:8080 { root * /path/to/your/app php_fastcgi unix//var/run/frankenphp.sock } ```
Then run frankenphp run to start the server. You can also use frankenphp --worker to enable worker mode. For production, you'll want to set up systemd service or use Docker. The official Docker image is available as dunglas/frankenphp.
Restart=always to handle crashes.Worker Mode Deep Dive
Worker mode is the flagship feature of FrankenPHP. Instead of starting a new PHP process for each request, a single script is loaded once and handles multiple requests sequentially. This dramatically reduces overhead, especially for frameworks with heavy bootstrapping (like Laravel or Symfony). To enable worker mode, add the worker directive to your Caddyfile or pass --worker flag. The script must be a PHP file that returns a callable or uses FrankenPHP\handleRequest function. Here's an example of a worker script:
```php <?php // worker.php require_once __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php'; $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
return function ($request) use ($kernel) { $response = $kernel->handle($request); $kernel->terminate($request, $response); return $response; }; ```
Then in Caddyfile: php_fastcgi unix//var/run/frankenphp.sock with worker directive. FrankenPHP will keep the script running and pass requests to it.
gc_collect_cycles() periodically in long-running workers to free cyclic references.Early Hints (103 Status Code)
Early hints allow the server to send preliminary HTTP headers before the final response, enabling the browser to start loading critical resources (like CSS, JS, images) earlier. FrankenPHP supports early hints via the 103 Early Hints status code. To use it, your application must send Link headers with rel=preload or rel=preconnect. FrankenPHP will automatically send these as early hints. For example, in a PHP script:
``php <?php header('Link: </style.css>; rel=preload; as=style'); // ... later generate the full response ``
FrankenPHP will send a 103 response with the Link header before the final 200 response. This can improve perceived performance significantly. You can also configure Caddy to add early hints for static assets.
Concurrent Request Handling
Unlike traditional PHP-FPM which uses multiple processes, FrankenPHP can handle multiple concurrent requests within a single PHP process using its Go-based event loop. This is achieved through the FrankenPHP\handleRequest function and coroutines. However, PHP itself is single-threaded, so concurrency is cooperative. You can use FrankenPHP\async to run non-blocking I/O operations. For example:
```php <?php use function FrankenPHP\async;
$result = async(function () { // Simulate non-blocking I/O return file_get_contents('https://api.example.com'); });
echo $result->await(); ```
This allows handling multiple requests while waiting for I/O. Note that this is not parallelism; it's concurrency within a single process. For CPU-bound tasks, you'll still need multiple workers.
sleep() in concurrent mode; use async equivalents.Security Best Practices with FrankenPHP
Running PHP in worker mode introduces security considerations. Since the application persists, any global state changes can affect subsequent requests. Ensure you reset global variables and close database connections after each request. Use register_shutdown_function to clean up. Also, be aware of file permissions: the worker runs as the user that started FrankenPHP. Use proper file ownership and avoid running as root. For HTTPS, FrankenPHP automatically provisions Let's Encrypt certificates via Caddy. Ensure your domain is correctly configured. Additionally, consider using OPcache to cache compiled PHP files. In worker mode, OPcache is especially beneficial because the cache persists across requests. Configure OPcache with opcache.enable=1 and opcache.revalidate_freq=0 for production.
The 10-Second Cold Start: How FrankenPHP Saved a Laravel API
- Worker mode eliminates cold starts by keeping the app in memory.
- Always profile first request latency separately.
- FrankenPHP is especially beneficial for frameworks with heavy bootstrapping.
- Combine with OPcache for maximum performance.
- Monitor memory usage in worker mode to avoid leaks.
memory_get_usage() at the end of requests. Consider restarting workers periodically.php -vfrankenphp version| File | Command / Code | Purpose |
|---|---|---|
| frankenphp-intro.php | echo "Hello from FrankenPHP! Request #" . ($_SERVER['REQUEST_ID'] ?? 'unknown') ... | What is FrankenPHP? |
| Caddyfile | { | Installation and Setup |
| worker.php | require_once __DIR__ . '/vendor/autoload.php'; | Worker Mode Deep Dive |
| early-hints.php | header('Link: ; rel=preload; as=style'); | Early Hints (103 Status Code) |
| concurrent.php | use function FrankenPHP\async; | Concurrent Request Handling |
| security.php | register_shutdown_function(function () { | Security Best Practices with FrankenPHP |
Key takeaways
Common mistakes to avoid
3 patternsNot resetting global state in worker mode
Using blocking I/O in concurrent mode
Ignoring memory growth in worker mode
Interview Questions on This Topic
Explain how worker mode in FrankenPHP improves performance.
Frequently Asked Questions
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
That's Advanced PHP. Mark it forged?
3 min read · try the examples if you haven't