Four Servers, One Framework
Unix socket to PHP-FPM.
PHP 8.3.6
PHP embedded in process.
PHP 8.3.6
Bundled PHP runtime.
PHP 8.5.5 (bundled)
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.
CI4 /ping Route — 3 Runs Averaged
Full CodeIgniter 4 stack — autoloading, service container, router, filters, and response building on every request.
All 3 Runs
| Server | Run 1 | Run 2 | Run 3 | Average | Avg Latency | Timeouts |
|---|---|---|---|---|---|---|
| Nginx + FPM | 372 | 388 | 386 | 382 | 249ms | 0 |
| Apache + mod_php | 371 | 365 | 376 | 371 | 259ms | 234 |
| Caddy + FPM | 330 | 330 | 355 | 338 | 282ms | 0 |
| FrankenPHP | 334 | 336 | 337 | 336 | 284ms | 0 |
Apache — mod_php vs PHP-FPM
Does switching Apache from mod_php to PHP-FPM improve performance?
FrankenPHP — Custom Worker Mode Attempts
Before discovering CI4's official worker support, we wrote our own worker scripts. Here's what happened.
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.
How it compares to everything else
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
$ 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.
php spark worker:install.Backed by the PHP Foundation.
Single binary. No PSR-7 required.
✓ Officially supported by CI4
No official CI4 integration package.
Better suited for Laravel/Symfony.
✗ Not officially supported by CI4
Notable Findings
Boot::bootWorker() path makes all the difference.Boot::bootWeb() actually hurt performance (211–219 req/sec). Proper worker mode requires CI4's own Boot::bootWorker() and framework-level state management methods.unix:/run/php/php8.3-fpm.sock over 127.0.0.1:9000 on single-server setups.Environment
| Server | DigitalOcean Droplet |
| CPU | 2 vCPU |
| RAM | 2 GB |
| OS | Ubuntu 24.04.4 LTS (Noble) |
| PHP (system) | 8.3.6 — Apache, Nginx, Caddy |
| PHP (FrankenPHP) | 8.5.5 — bundled binary |
| Framework | CodeIgniter 4.7.2 (fresh install) |
| Apache | 2.4.58 — mod_php, prefork MPM — :80 |
| Nginx | latest — PHP-FPM Unix socket — :8081 |
| Caddy | latest — PHP-FPM Unix socket — :8082 |
| FrankenPHP classic | v1.12.2 — :8083 |
| FrankenPHP worker | v1.12.2 — php spark worker:install — :8080 |
| RoadRunner | Excluded — no official CI4 integration |
| OPcache | Enabled — validate_timestamps=0, max_files=10000 |
| Benchmark tool | wrk -t8 -c100 -d30s |
| Benchmark route | GET /ping → returns "pong" (no DB) |
| CI4 Worker docs | codeigniter.com/user_guide/installation/running.html |
$ 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