Performance

FrankenPHP: PHP's High-Performance New Runtime

FrankenPHP bundles PHP into a Go-powered app server with worker mode, early hints, and automatic HTTPS. Here is how to use it and when it beats php-fpm.

FrankenPHP runtime architecture with worker mode diagram

PHP-FPM has been the default way to run PHP since 2009. It works, it is battle-tested, and it is boring in the best sense. FrankenPHP changes the equation by embedding PHP directly into a Go-based application server (Caddy), adding worker mode that keeps PHP processes alive between requests, and providing automatic HTTPS, HTTP/3, and 103 Early Hints support without any Nginx or Apache configuration.

The performance gains are real. But so are the operational differences. This guide covers what FrankenPHP actually does, when it helps, and where php-fpm remains the safer choice.

What FrankenPHP actually is

FrankenPHP is a PHP SAPI (Server API) implemented as a Go module for the Caddy web server. In practical terms:

  • It replaces Nginx + php-fpm with a single binary
  • It serves static files and PHP simultaneously
  • It handles TLS certificate provisioning automatically (via Caddy’s ACME integration)
  • It supports HTTP/1.1, HTTP/2, and HTTP/3 out of the box
  • It can run in standard mode (request-per-process, like php-fpm) or worker mode (persistent processes)

Architecture

1
2
3
4
5
6
7
8
                  ┌─────────────┐
Client ──HTTP/3──▶│ Caddy │
│ (Go runtime) │
│ ┌─────────┐ │
│ │FrankenPHP│ │
│ │ (PHP) │ │
│ └─────────┘ │
└─────────────┘

There is no separate web server process, no FastCGI socket, and no reverse proxy configuration. The PHP runtime runs inside the same process as the web server.

Installation

Binary download

1
2
3
4
5
6
# Download the standalone binary (includes PHP)
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o frankenphp
chmod +x frankenphp

# Or with Docker
docker run -v $PWD:/app -p 443:443 dunglas/frankenphp

From Composer (for Laravel)

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

Laravel Octane provides a first-class integration that handles the worker mode lifecycle, including resetting application state between requests.

Standard mode (drop-in replacement)

In standard mode, FrankenPHP behaves like php-fpm. Each request starts a fresh PHP process, executes the script, and tears down. The advantage over Nginx + php-fpm is simplicity: one process, automatic HTTPS, no socket configuration.

1
2
3
4
5
# Serve a PHP application
frankenphp php-server --root /var/www/public

# With a custom Caddyfile for more control
frankenphp run --config Caddyfile
1
2
3
4
5
# Caddyfile
example.com {
root * /var/www/public
php_server
}

That is the entire web server configuration. Caddy handles TLS certificates from Let’s Encrypt automatically.

Performance in standard mode

Standard mode performance is comparable to php-fpm. You are not gaining speed—you are gaining operational simplicity. One binary replaces three (Nginx, php-fpm, certbot).

Worker mode

Worker mode is where the performance gains happen. Instead of starting a new PHP process per request, FrankenPHP keeps PHP workers alive. The application boots once (framework bootstrap, service container, configuration loading) and then handles multiple requests without the boot overhead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// frankenphp-worker.php
<?php

use App\Kernel;

require __DIR__ . '/../vendor/autoload.php';

$kernel = new Kernel();
$kernel->boot();

// This loop handles incoming requests without rebooting
$handler = static function () use ($kernel) {
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
};

// FrankenPHP worker loop
while ($request = \frankenphp_handle_request($handler)) {
// Each iteration handles one HTTP request
// PHP process stays alive between requests
}

Performance in worker mode

For a typical Laravel application:

Metric php-fpm FrankenPHP standard FrankenPHP worker
Requests/sec ~800 ~850 ~2,400
Avg response time 12ms 11ms 4ms
P99 response time 45ms 42ms 15ms
Memory per worker 40MB 38MB 55MB

Worker mode roughly triples throughput because it eliminates the framework boot cost on every request. The tradeoff is higher per-worker memory usage (the application stays loaded).

The state persistence trap

This is the most important thing to understand about worker mode. Because the PHP process persists between requests, any global state leaks between requests:

1
2
3
4
5
6
7
8
9
10
// This is a bug in worker mode
class RequestCounter
{
private static int $count = 0;

public static function increment(): void
{
self::$count++; // Persists across requests!
}
}

What breaks:

  • Static variables that accumulate
  • Singletons that cache request-specific data
  • Database connections that assume per-request lifecycle
  • Global variables
  • $_SESSION, $_GET, $_POST superglobals (must be reset)

What works fine:

  • Laravel/Symfony with Octane (they reset state automatically)
  • Stateless service classes
  • Applications designed for long-running processes

103 Early Hints

FrankenPHP supports HTTP 103 Early Hints, which lets the server send preload directives before the response is ready:

1
2
3
4
5
6
7
8
9
10
11
// Send early hints while the page is still rendering
frankenphp_request_headers();

header('Link: </css/app.css>; rel=preload; as=style', false, 103);
header('Link: </js/app.js>; rel=preload; as=script', false, 103);

// Now do the expensive work
$html = renderPage();

// Send the actual response
echo $html;

The browser starts downloading CSS and JS while PHP is still rendering the HTML. This can reduce perceived load time by 100-300ms on complex pages.

Docker deployment

1
2
3
4
5
6
7
8
9
10
11
12
FROM dunglas/frankenphp:latest

COPY . /app

RUN install-php-extensions \
pdo_mysql \
redis \
opcache

WORKDIR /app/public

ENTRYPOINT ["frankenphp", "run", "--config", "/app/Caddyfile"]
1
2
3
4
5
6
7
8
9
10
11
12
{
frankenphp
order php_server before file_server
}

:443 {
root * /app/public
php_server {
worker /app/public/frankenphp-worker.php
num 10
}
}

Common mistakes

Enabling worker mode without testing state leaks: Always run your test suite in worker mode, not just standard mode. Tests that pass per-request may fail when state persists.

Not configuring worker count: The default worker count may be too high for memory-constrained servers. Each worker holds the full application in memory. Set num based on available RAM.

Ignoring health checks: Worker mode processes can become stale. Configure health check endpoints that verify the application is responsive, not just that the process is running.

Treating FrankenPHP as a CDN replacement: It serves static files well but lacks edge caching. Use Cloudflare, Fastly, or another CDN in front of FrankenPHP for static assets.

When to choose FrankenPHP vs. alternatives

Use case FrankenPHP php-fpm + Nginx Swoole/RoadRunner
Simple deployment Moderate setup Complex
Auto HTTPS ✅ Built-in Need certbot Need reverse proxy
Worker mode performance ✅ ~3x Baseline ✅ ~3-4x
Ecosystem maturity Growing Battle-tested Moderate
PHP extension compat Most work All work Some need patches
Shared hosting No Yes No

FAQ

Does FrankenPHP work with WordPress?

Yes, in standard mode. Worker mode requires modifications to WordPress’s global-state-heavy architecture that are not practical for most sites.

Can I use FrankenPHP with existing Nginx configs?

No. FrankenPHP replaces Nginx entirely. You would rewrite your server configuration as a Caddyfile. For complex Nginx configurations, this can be significant work.

Is FrankenPHP production-ready?

For standard mode, yes. Many production sites use it. Worker mode is stable for Laravel Octane and Symfony Runtime. Custom worker implementations need thorough testing.

Next steps

Install FrankenPHP locally and run your existing PHP application in standard mode. If it works without issues (it should), benchmark it against your current setup. Then evaluate whether worker mode’s performance gains justify the testing effort.

For understanding the performance fundamentals that FrankenPHP optimizes, the REST API best practices guide covers how API design interacts with server performance. The PHP 8.5/8.6 features guide covers language-level improvements that complement runtime optimizations.