Benchmark Report

PHP Server
Showdown

Apache · Nginx · Caddy · FrankenPHP — tested against a real CodeIgniter 4 application on a clean Ubuntu 24.04 droplet. Now includes CI4's official FrankenPHP worker mode.

Framework CodeIgniter 4.7.2
Server DigitalOcean — 2 vCPU / 2GB
OS Ubuntu 24.04 LTS
Tool wrk -t8 -c100 -d30s

Four Servers, One Framework

:8081
Nginx + PHP-FPM
Event-driven architecture.
Unix socket to PHP-FPM.
PHP 8.3.6
:80
Apache + mod_php
Prefork MPM.
PHP embedded in process.
PHP 8.3.6
:8083
FrankenPHP
Built on Caddy. Single binary.
Bundled PHP runtime.
PHP 8.5.5 (bundled)
:8082
Caddy + PHP-FPM
Automatic HTTPS. Go-based.
Unix socket to PHP-FPM.
PHP 8.3.6

Raw PHP — bench.php

A plain echo "ok" file served directly — no framework, no routing. Pure server + PHP overhead.

Requests / second — higher is better
Nginx + FPM
6,262
15ms avg
Apache + mod_php
5,972
118ms avg ⚠
FrankenPHP
4,978
19ms avg
Caddy + FPM
3,259
29ms avg
Key insight: Nginx wins on raw throughput with rock-solid consistency (2.88ms stdev). Apache matches on req/sec but shows alarming latency spikes — 231ms stdev means wildly inconsistent response times. FrankenPHP performs well despite running a newer, less-optimized PHP 8.5.5.

CI4 /ping Route — 3 Runs Averaged

Full CodeIgniter 4 stack — autoloading, service container, router, filters, and response building on every request.

Average Requests / second over 3 runs — higher is better
Nginx + FPM
382
0 timeouts
Apache + mod_php
371
234 timeouts
Caddy + FPM
338
0 timeouts
FrankenPHP
336
0 timeouts

All 3 Runs

Server Run 1 Run 2 Run 3 Average Avg Latency Timeouts
Nginx + FPM 372388386 382 249ms 0
Apache + mod_php 371365376 371 259ms 234
Caddy + FPM 330330355 338 282ms 0
FrankenPHP 334336337 336 284ms 0
Key insight: All four servers converge within ~50 req/sec of each other. CI4 is the real bottleneck — the framework overhead dominates regardless of server. Apache's 234 timeouts across 3 runs is a serious reliability concern in production.

Apache — mod_php vs PHP-FPM

Does switching Apache from mod_php to PHP-FPM improve performance?

Requests / second — higher is better
mod_php
378
99 timeouts
PHP-FPM run 2
356
6 timeouts
PHP-FPM run 1
339
89 timeouts
Key insight: mod_php outperforms PHP-FPM on Apache because PHP runs inside the Apache worker process, eliminating Unix socket IPC overhead entirely. However PHP-FPM dramatically reduces timeouts (99 → 6). It's a throughput vs reliability tradeoff.

FrankenPHP — Custom Worker Mode Attempts

Before discovering CI4's official worker support, we wrote our own worker scripts. Here's what happened.

Requests / second — higher is better
Classic mode
365
260ms avg
Worker (no reset)
219
433ms avg
Worker (with reset)
211
450ms avg
What went wrong: Our custom worker used Boot::bootWeb() which is designed for single-request lifecycles, not persistent workers. Calling Services::reset(true) and Factories::reset() between requests was expensive enough to negate any gain. The right approach requires a dedicated boot path — which CI4 now provides officially.

FrankenPHP — Official CI4 Worker Mode

CodeIgniter 4.7.2 ships with official FrankenPHP worker mode support via php spark worker:install. This generates a proper frankenphp-worker.php and Caddyfile using Boot::bootWorker() — a dedicated boot path that handles state, DB reconnection, superglobals, and session cleanup correctly between requests.

⚠ Experimental: Per the official CI4 documentation, Worker Mode is currently experimental. The only officially supported worker implementation is FrankenPHP, which is backed by the PHP Foundation. Use in production with caution.
Requests / second — 3 runs — higher is better
Official Worker R1
517
184ms avg
Official Worker R2
514
185ms avg
Official Worker R3
520
183ms avg

How it compares to everything else

Final leaderboard — Requests / second — higher is better
FrankenPHP Worker ★
517 avg
0 timeouts
Nginx + FPM
382
0 timeouts
Apache + mod_php
371
234 timeouts
Caddy + FPM
338
0 timeouts
FrankenPHP Classic
336
0 timeouts
Key insight: The official CI4 worker mode delivers 517 req/sec — a 54% improvement over FrankenPHP classic and 35% faster than Nginx. The difference from our custom worker script is that CI4 uses Boot::bootWorker(), a dedicated persistent-process boot path, plus framework-level methods like resetForWorkerMode(), DatabaseConfig::reconnectForWorkerMode(), and proper superglobal refresh — none of which were available in our manual attempt.

Quick Start — Official CI4 Worker Mode

# 1. Install FrankenPHP binary
$ curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 \
  -o /usr/local/bin/frankenphp && chmod +x /usr/local/bin/frankenphp

# 2. Generate CI4 worker files (creates Caddyfile + frankenphp-worker.php)
$ php spark worker:install

# 3. Start the server
$ frankenphp run

Why RoadRunner Was Excluded

RoadRunner is often mentioned as FrankenPHP's main competition in the PHP application server space. We considered including it but ultimately excluded it for a principled reason.

No official CI4 + RoadRunner integration exists. RoadRunner uses a PSR-7 request/response interface, while CodeIgniter 4's HTTP layer is not PSR-7 compliant. Benchmarking RoadRunner without CI4 — just returning a plain "pong" response — would measure RoadRunner's raw worker loop speed, not a meaningful CI4 application scenario. That would be an unfair and misleading comparison.
FrankenPHP
Official CI4 support via php spark worker:install.
Backed by the PHP Foundation.
Single binary. No PSR-7 required.
✓ Officially supported by CI4
RoadRunner
Requires PSR-7 compatibility layer.
No official CI4 integration package.
Better suited for Laravel/Symfony.
✗ Not officially supported by CI4

Notable Findings

01
Official worker mode is a game changer
FrankenPHP + CI4's official worker script delivers 517 req/sec — 35% faster than Nginx and 54% faster than FrankenPHP classic. The framework's dedicated Boot::bootWorker() path makes all the difference.
02
CI4 is the bottleneck — until it isn't
Without worker mode: all servers converge at 330–382 req/sec. With official worker mode: FrankenPHP breaks away to 517 req/sec, proving the framework overhead can be eliminated with the right approach.
03
Nginx is the best traditional choice
If you can't use FrankenPHP worker mode: fastest req/sec, lowest latency stdev, zero timeouts. The clear winner among traditional server setups.
04
Apache has a timeout problem
234 total timeouts across 3 CI4 runs with mod_php. The prefork MPM struggles under sustained high concurrency — a real production concern regardless of performance.
05
DIY worker scripts don't work
Our custom worker using Boot::bootWeb() actually hurt performance (211–219 req/sec). Proper worker mode requires CI4's own Boot::bootWorker() and framework-level state management methods.
06
Worker mode is still experimental
Per official CI4 docs, worker mode is experimental. Only FrankenPHP is officially supported — backed by the PHP Foundation. RoadRunner has no official CI4 integration due to PSR-7 incompatibility.
07
Unix socket beats TCP
PHP-FPM via Unix socket eliminates network stack overhead. Always prefer unix:/run/php/php8.3-fpm.sock over 127.0.0.1:9000 on single-server setups.
08
Caddy overhead is real
Both Nginx and Caddy use PHP-FPM via Unix socket yet Nginx delivers ~6,262 req/sec vs Caddy's ~3,259 on raw PHP. Caddy's Go-based request handling adds measurable overhead compared to Nginx's C implementation.

Environment

ServerDigitalOcean Droplet
CPU2 vCPU
RAM2 GB
OSUbuntu 24.04.4 LTS (Noble)
PHP (system)8.3.6 — Apache, Nginx, Caddy
PHP (FrankenPHP)8.5.5 — bundled binary
FrameworkCodeIgniter 4.7.2 (fresh install)
Apache2.4.58 — mod_php, prefork MPM — :80
Nginxlatest — PHP-FPM Unix socket — :8081
Caddylatest — PHP-FPM Unix socket — :8082
FrankenPHP classicv1.12.2 — :8083
FrankenPHP workerv1.12.2 — php spark worker:install — :8080
RoadRunnerExcluded — no official CI4 integration
OPcacheEnabled — validate_timestamps=0, max_files=10000
Benchmark toolwrk -t8 -c100 -d30s
Benchmark routeGET /ping → returns "pong" (no DB)
CI4 Worker docscodeigniter.com/user_guide/installation/running.html
# Install wrk and run benchmark
$ sudo apt install -y wrk
$ wrk -t8 -c100 -d30s http://localhost/ping

# Full 3-run loop used for final results
$ for i in 1 2 3; do
  echo "=== Run $i ==="
  wrk -t8 -c100 -d30s http://localhost/ping
done