Image variations

FrankenPHP

Run the same application classic or in Octane worker mode, and measure the difference.

FrankenPHP is a PHP application server built on Caddy. It serves PHP and HTTP from a single process, so there is no PHP-FPM pool and no separate web server to supervise.

Its headline feature is worker mode: the framework boots once and stays in memory, instead of bootstrapping on every request. BenchKit can run the same image both ways, which is the cleanest comparison the tool can make.

Two modes, one image

ModeWhat happens per requestHow you start it
ClassicThe framework boots, serves, and tears down, the way PHP-FPM doesDefault command
WorkerThe framework is already booted and stays residentphp artisan octane:start
docker run -p 80:8080 \
  -v benchkit-runs:/var/www/html/storage/app/runs \
  serversideup/benchkit-laravel:frankenphp

BenchKit detects which mode served the run and labels the result. A worker mode run and a classic run are never mistaken for each other.

Running the same image both ways is the cleanest comparison you can make with BenchKit. Nothing changes except whether the framework boots per request, so the difference between the two runs is what worker mode is worth on that machine.

Health checks in worker mode

The image's built-in health check is wired for the classic setup. Octane starts its own server with its own Caddy configuration, so that check starts failing once worker mode takes over. The image ships a healthcheck-octane script for exactly this case.

If Docker Compose or an orchestrator is watching container health, point the check at that script:

docker-compose.yml
services:
  benchkit:
    image: serversideup/benchkit-laravel:frankenphp
    ports:
      - "80:8080"
    command: ["php", "artisan", "octane:start", "--server=frankenphp", "--host=0.0.0.0", "--port=8080"]
    healthcheck:
      test: ["CMD", "healthcheck-octane"]
      start_period: 10s
    volumes:
      - benchkit-runs:/var/www/html/storage/app/runs

volumes:
  benchkit-runs:

With a plain docker run, nothing acts on health status. The container shows as unhealthy in docker ps, which you can ignore or silence with --no-healthcheck.

Reading the comparison fairly

Worker mode's saving is the framework bootstrap, and that is a fixed amount of work per request. So the saving looks largest on fast, CPU-bound routes, and it is a smaller share of a request that spends most of its time waiting on a database or an API.

That is a claim about proportion, and it is easy to get wrong, because two other things move at the same time:

PHP-FPM's pool size caps concurrency. Under FPM a request occupies a worker for its whole duration, including time spent waiting. On the simulated I/O route, a pool of 20 workers cannot exceed a fixed ceiling however fast the hardware is. If your connection count is above pm.max_children, you are measuring the pool rather than the framework. BenchKit detects the pool size and flags the run when this happens.

Neither side is tuned by default. A stock FPM pool against a stock FrankenPHP is not a verdict on either one. Compare tuned against tuned, and say which tuning you used.

Before you publish a FrankenPHP-versus-FPM comparison, look at the /bench/io curve on the FPM side. If it flattens early, the worker pool is what you measured rather than the server — see Common tuning strategies. The most common way to produce a dramatic and wrong result is to compare a tuned worker mode against a default pool.
Run the suite on FPM-NGINX and on FrankenPHP worker mode, open both in Compare, then raise the Simulated I/O response and run again. Watching how the gap changes as I/O grows tells you more than either number on its own. See Methodology.