FrankenPHP, OPcache JIT, and Preloading for Laravel
FrankenPHP is not just a novelty — it embeds PHP directly into a Caddy-based server and exposes a worker mode that keeps your Laravel application bootstrapped between requests, similar to Octane but without a separate process manager. Pair that with a well-tuned OPcache preload script and the right JIT mode and you have a meaningful throughput story without exotic infrastructure.
This article is about the specific, practical configuration decisions that matter — not a feature tour.
Worker Mode vs. Traditional Mode
In traditional mode FrankenPHP behaves like php-fpm: each request boots the framework from scratch. Worker mode changes this fundamentally:
// frankenphp-worker.php (placed at project root)
<?php
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// Signal FrankenPHP that the worker is ready
frankenphp_handle_request(function () use ($app, $kernel) {
$request = Illuminate\Http\Request::capture();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
});
The closure runs on every request; everything outside it runs once per worker lifetime. This is where state leakage lives — static properties, singleton bindings that cache user-specific data, and anything written to $_SERVER without cleanup will bleed across requests.
Rule of thumb: if you would not trust it in Octane, do not trust it here.
OPcache Preloading
Preloading compiles and caches PHP files into shared memory at server start. For Laravel this means the framework core, your service providers, and hot domain classes are already compiled before the first request arrives.
; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.preload=/var/www/html/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->getRealPath());
}
}
// Also preload your own hot paths
opcache_compile_file(__DIR__ . '/app/Models/User.php');
opcache_compile_file(__DIR__ . '/app/Http/Kernel.php');
Important:
opcache.validate_timestamps=0means OPcache never checks disk for changes. You must restart the server after every deploy. Automate this or you will serve stale code.
Choosing the Right JIT Mode
PHP's JIT has two practical modes:
| Mode | opcache.jit value | Best for |
|---|---|---|
| Tracing | trasm / 1255 | CPU-bound loops, math-heavy code |
| Function | function / 1205 | General web workloads |
For a typical Laravel request — mostly I/O waiting on MySQL and Redis — JIT provides modest gains. The real wins come from preloading and worker mode. That said, if you run CPU-intensive transformations (image processing, CSV parsing, report generation) inside the same process, tracing JIT is worth enabling:
opcache.jit=trasm
opcache.jit_buffer_size=128M
Do not set jit_buffer_size larger than you need. Oversized buffers waste shared memory and can cause allocation failures on low-RAM servers.
Caddyfile Configuration
{
frankenphp
order php_server before file_server
}
localhost {
root * /var/www/html/public
php_server {
worker /var/www/html/frankenphp-worker.php
num_workers 8
}
}
num_workers should match your CPU core count as a starting point. Under FrankenPHP each worker is a long-lived PHP thread, not a process — memory is shared more efficiently than fpm pools.
Practical Takeaways
- Worker mode eliminates framework bootstrap cost; guard against static state leakage the same way you would with Octane.
- Preloading with
validate_timestamps=0is safe in production only when your deploy pipeline restarts the server. - JIT tracing mode helps CPU-bound work; for pure I/O-heavy Laravel apps the gain is real but modest — measure before claiming wins.
- Keep
opcache.memory_consumptiongenerous; evictions under pressure negate the benefit entirely. - Profile with
opcache_get_status()in a protected endpoint to verify preloaded file counts and hit rates after deploy.