6 min read

FrankenPHP Worker Mode: Boot Once, Serve Everything

FrankenPHP is now an official PHP Foundation project. Here's how worker mode delivers 15,000 req/sec and what it means for your Laravel and Symfony apps.

Featured image for "FrankenPHP Worker Mode: Boot Once, Serve Everything"

If you’ve been running PHP-FPM since 2010 and accepting ~4,000 requests per second as a reasonable ceiling, FrankenPHP is the correction you’ve been waiting for. And now that the PHP Foundation has officially adopted it as a project under the PHP organization’s GitHub umbrella, this isn’t a niche experiment anymore — it’s where serious PHP infrastructure is heading.

Here’s what you need to know, including the part most tutorials skip: worker mode, and what it actually changes about how your application runs.

What FrankenPHP Is (and Isn’t)

FrankenPHP is a PHP application server written in Go, created by Kévin Dunglas and sponsored by Les-Tilleuls.coop — the team behind the API Platform framework. It embeds PHP directly into a Go binary, which means it doesn’t talk to PHP-FPM over FastCGI. It is PHP, compiled in. The server handles HTTP/2 and HTTP/3 natively, terminates TLS without a separate Nginx or Caddy layer, and includes Early Hints support out of the box.

In early 2026, the PHP Foundation announced FrankenPHP’s source code would be transferred to the php/frankenphp repository under the PHP project’s GitHub organization, and that Foundation-sponsored developers would actively contribute to its maintenance. That’s the difference between a well-maintained third-party tool and infrastructure backed by the language itself.

The same month, Windows native support shipped — including worker mode and hot reloading on Windows Server — delivering a 3.6x performance improvement over optimized Nginx/PHP-FPM configurations. If your team runs development locally on Windows, the excuse to skip it is gone.

The Part That Actually Matters: Worker Mode

Classic PHP execution is stateless by design. PHP-FPM spins up a process for each request, loads your index.php, bootstraps your framework, executes the request, and tears everything down. Every request. That bootstrap cycle — parsing autoloaders, hydrating service containers, compiling route tables — is not free. On a large Laravel application with a deep service provider tree, it’s measurably expensive.

Worker mode changes the contract. FrankenPHP boots your application once. The service container, compiled routes, and cached views stay in memory. Subsequent requests skip the bootstrap entirely and go straight to route resolution. FrankenPHP achieves approximately 15,000 requests per second in worker mode benchmarks. PHP-FPM manages about 4,000 under the same conditions. That’s a 3.75x throughput improvement with no changes to your application logic.

The catch: your application has to handle this correctly. State that was implicitly reset between requests (because PHP process dies = state dies) now persists between requests. You have to be deliberate about what you reset.

The good news for Laravel and Symfony users: both frameworks have integrated FrankenPHP worker mode officially, and they handle the reset cycle for you.

Setting Up Laravel with FrankenPHP Worker Mode

Laravel’s integration runs through Laravel Octane. Install it, tell it to use FrankenPHP, and the framework handles request lifecycle management:

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

For production, the official Docker image is the recommended path:

FROM dunglas/frankenphp:latest-php8.5

# Copy application
COPY . /app

# Install dependencies
RUN composer install --no-dev --optimize-autoloader

# Worker mode config
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
ENV SERVER_NAME=":80"

# Laravel optimization
RUN php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

The FRANKENPHP_CONFIG environment variable is the key line. It tells FrankenPHP to start in worker mode using your Laravel entry point. Your routes, service providers, and compiled caches load once at container start.

For local development, the Octane serve command wraps all of this:

php artisan octane:serve --server=frankenphp --port=8000 --watch

The --watch flag handles hot reloading during development — file changes trigger a graceful worker restart without losing in-flight requests.

Symfony Integration

Symfony’s FrankenPHP integration ships via the runtime/frankenphp-symfony package. Most Symfony applications work in worker mode without code changes. The framework implements ResetInterface on its services, which tells FrankenPHP what to clean up between requests.

composer require runtime/frankenphp-symfony

Then in your Dockerfile:

FROM dunglas/frankenphp:latest-php8.5

COPY . /app

RUN composer install --no-dev --optimize-autoloader \
    && php bin/console cache:warmup --env=prod

ENV APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime
ENV FRANKENPHP_CONFIG="worker ./public/index.php"

The APP_RUNTIME environment variable routes Symfony’s application runtime through the FrankenPHP-aware adapter. One developer who migrated a large Symfony application (~1,000 controllers and services) reported implementing ResetInterface on exactly one service that cached data across request boundaries. The rest worked without modification.

What State You Actually Need to Reset

If you’re not using Laravel or Symfony’s managed integration, you need to handle the worker loop yourself. Here’s the minimal pattern:

<?php
// public/worker.php

$handler = static function() {
    // Your application bootstrap (runs ONCE)
    $app = require __DIR__ . '/../bootstrap/app.php';

    return static function() use ($app): void {
        // This closure runs for every request
        $request = Request::createFromGlobals();
        $response = $app->handle($request);
        $response->send();

        // Reset state that must not bleed between requests
        $app->reset();
    };
};

frankenphp_handle_request($handler());

The frankenphp_handle_request() function is the core primitive. It accepts a callable, blocks until a request arrives, executes your closure in the context of that request, then loops. Your bootstrap code runs in the outer closure — once. Your request handling code runs in the inner closure — every time.

The state problems to watch for: static properties on services, singleton registries that accumulate entries per request, database connection state that should be rolled back on failure, and any global that your framework doesn’t explicitly reset. Laravel Octane’s documentation includes a list of common pitfalls worth reviewing before going to production.

Multi-App and HTTPS Without a Proxy

One underappreciated feature is that FrankenPHP includes Caddy’s automatic HTTPS support. You can serve multiple applications from a single FrankenPHP instance with TLS termination handled natively, without a reverse proxy layer:

{
  "apps": {
    "http": {
      "servers": {
        "main": {
          "listen": [":443"],
          "routes": [
            {
              "match": [{ "host": ["api.example.com"] }],
              "handle": [{ "handler": "php", "root": "/var/www/api/public" }]
            },
            {
              "match": [{ "host": ["app.example.com"] }],
              "handle": [{ "handler": "php", "root": "/var/www/app/public" }]
            }
          ]
        }
      }
    }
  }
}

Certificates are provisioned and renewed automatically via Let’s Encrypt. For internal services, FrankenPHP generates self-signed certificates trusted by its built-in CA — the same approach Caddy uses for local HTTPS development.

Is It Ready for Production?

Short answer: yes, with caveats you’d apply to any high-performance runtime. The PHP Foundation’s formal adoption and the integration with Laravel and Symfony are the strongest signals that this has cleared the “interesting experiment” threshold. The 15,000 req/sec benchmarks are real, though your actual numbers depend heavily on application complexity, database I/O, and whether your services implement proper request boundary resets.

The teams running FrankenPHP in production are reporting meaningful infrastructure cost reductions — fewer instances needed to handle the same traffic. The tradeoff is that the worker mode mental model requires care upfront. A stateful bug in worker mode can cause request cross-contamination, which is a category of issue PHP developers haven’t traditionally had to think about. Read the Octane upgrade guide before deploying. Test under load before trusting the static analysis.

The trajectory is clear. FrankenPHP is not an alternative to PHP’s standard runtime — it’s the direction the runtime is moving.

Sources