Why FrankenPHP Changes the Equation
FrankenPHP embeds a PHP runtime directly into a Caddy-based binary. Unlike Swoole or RoadRunner, it requires no PHP extension and no separate process manager — the worker lifecycle is handled by the server itself. Combined with OPcache preloading and JIT compilation, it can serve Laravel applications with latency profiles that rival Node.js, without rewriting a line of application code.
This article focuses on three levers: worker mode, preload scripts, and JIT configuration. Each has sharp edges worth knowing before you push to production.
Worker Mode: Persistent Application Bootstrap
FrankenPHP's worker mode boots your Laravel application once and reuses it across requests — the same model Octane uses, but without the Octane dependency.
// frankenphp-worker.php (placed at public/)
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
while (\frankenphp_handle_request(function () use ($app, $kernel) {
$request = Illuminate\Http\Request::capture();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
})) {
// loop continues; add per-request cleanup here if needed
gc_collect_cycles();
}
The Caddyfile wires it up:
{
frankenphp
}
localhost {
root * /var/www/public
php_server {
worker /var/www/public/frankenphp-worker.php 4
}
}
The integer 4 is the number of worker processes. Start at (CPU cores × 2) and profile from there.
State leakage is real. Anything stored in a static property or a singleton that is not reset between requests will bleed across users. Audit your service providers and reset state in the worker loop or via $app->forgetInstance().
OPcache Preloading
Preloading compiles and caches PHP files into shared memory at server start, eliminating per-request compilation overhead for your hottest code paths.
; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; production only
opcache.preload=/var/www/preload.php
opcache.preload_user=www-data
A minimal preload script for Laravel:
<?php
// preload.php
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(__DIR__ . '/vendor/laravel/framework/src')
);
foreach ($files as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
opcache_compile_file($file->getPathname());
}
}
Preloading the framework core (≈1,200 files) typically saves 2–4 ms per cold-path request. Preloading your own app/ directory adds marginal benefit unless your domain layer is large.
Gotcha:
opcache.validate_timestamps=0means file changes are invisible until the server restarts. Never enable this in development.
JIT: Tracing vs Function Mode
PHP's JIT compiler operates in two main modes:
| Mode | opcache.jit value | Best for |
|---|---|---|
| Tracing | 1255 | Long-running loops, math-heavy code |
| Function | 1205 | General web workloads |
For typical Laravel request/response cycles, function mode (1205) is the safer default. Tracing JIT can regress performance on short-lived, branch-heavy code because the tracing overhead outweighs the gains.
opcache.jit=1205
opcache.jit_buffer_size=128M
Verify JIT is active:
php -r "var_dump(opcache_get_status()['jit']['enabled']);"
# bool(true)
Profile with Blackfire before and after enabling JIT. On pure I/O-bound Laravel apps the improvement is modest (5–15%). On CPU-bound workloads — PDF generation, image processing, complex collection pipelines — gains can exceed 30%.
Production Checklist
- Disable
validate_timestampsin production; add a deploy step that callsopcache_reset()or restarts FrankenPHP workers. - Pin worker count to a value you've load-tested; over-provisioning wastes memory without throughput gains.
- Reset bound singletons between requests when using worker mode — especially auth guards and request-scoped services.
- Monitor JIT buffer exhaustion via
opcache_get_status()['jit']['buffer_free']; increasejit_buffer_sizeif it approaches zero. - Test preload scripts in CI — a syntax error in a preloaded file crashes the entire server process.
Key Takeaways
- FrankenPHP worker mode eliminates bootstrap overhead without requiring Octane or Swoole.
- Preloading the framework core is low-risk and measurably reduces per-request compilation time.
- JIT function mode (
1205) is the right default for Laravel; tracing mode requires profiling to justify. - State leakage between requests is the primary operational risk of any persistent-worker model.
- Always benchmark with realistic traffic shapes before attributing gains to any single lever.