Why FrankenPHP Changes the Equation
Traditional PHP-FPM spawns a fresh process per request, discarding compiled bytecode between requests unless OPcache is warm. FrankenPHP embeds PHP directly into a Caddy-based server and, crucially, supports worker mode — a long-lived PHP process that boots Laravel once and handles thousands of requests before recycling. That single architectural shift makes JIT and preloading dramatically more valuable.
OPcache Preloading in Laravel
Preloading compiles a set of PHP files into shared memory at server start. Every worker process inherits that memory map, skipping file I/O and compilation entirely for preloaded classes.
Create a preload script at bootstrap/preload.php:
<?php
require __DIR__ . '/../vendor/autoload.php';
// Preload the framework kernel and heavy Eloquent internals
$files = [
__DIR__ . '/../vendor/laravel/framework/src/Illuminate/Foundation/Application.php',
__DIR__ . '/../vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php',
__DIR__ . '/../vendor/laravel/framework/src/Illuminate/Http/Request.php',
// Add your own high-traffic domain classes here
];
foreach ($files as $file) {
opcache_compile_file($file);
}
Then wire it in php.ini:
opcache.enable=1
opcache.preload=/var/www/bootstrap/preload.php
opcache.preload_user=www-data
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
Tip: Keep the preload list focused. Preloading every vendor file inflates shared memory and can hurt cold-start time on low-memory hosts.
Enabling JIT Tracing
PHP's JIT compiler operates in two modes: function (simpler, lower overhead) and tracing (follows hot execution paths across call boundaries). For Laravel's request cycle — which is heavy on method dispatch and Eloquent hydration — tracing JIT consistently outperforms function JIT.
opcache.jit=tracing
opcache.jit_buffer_size=128M
The tracing shorthand maps to the numeric value 1254. Avoid on (which maps to function JIT) in production Laravel apps.
FrankenPHP Worker Mode
FrankenPHP's worker mode is configured via a Caddyfile or environment variable:
{
frankenphp
}
localapp.test {
root * /var/www/public
php_server {
worker /var/www/public/index.php
num 8
}
}
The worker directive points to your Laravel entry point. FrankenPHP calls it once, then loops internally. Laravel's service container, config, and routes are resolved on the first invocation only.
Your public/index.php needs a small adaptation to signal the loop:
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
// Boot once
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// FrankenPHP worker loop
while ($request = \FrankenPHP\acceptRequest()) {
$laravelRequest = Illuminate\Http\Request::capture();
$response = $kernel->handle($laravelRequest);
$response->send();
$kernel->terminate($laravelRequest, $response);
}
State Leakage — The Real Risk
Long-lived workers share in-process state. Common pitfalls:
- Static properties on service classes that accumulate per-request data.
- Singleton bindings that cache user-specific data (e.g., a
CurrentTenantsingleton resolved from the request). - Eloquent global scopes registered conditionally during a request.
Audit every singleton in your AppServiceProvider. Prefer scoped() bindings for anything touched by request context — FrankenPHP respects Laravel's scoped container reset between requests when you call $kernel->terminate().
Practical Throughput Expectations
Without fabricating benchmarks: the combination of worker mode + preloading eliminates the two most expensive phases of a PHP request (autoload resolution and container bootstrap). JIT tracing adds measurable gains for CPU-bound work like collection transformations and complex Eloquent hydration, but has minimal impact on I/O-bound endpoints. Profile with Blackfire before and after to identify where your specific application benefits most.
Key Takeaways
- Use tracing JIT (
opcache.jit=tracing) for Laravel, not function JIT. - Keep preload scripts targeted — preloading everything wastes shared memory.
- FrankenPHP worker mode eliminates per-request bootstrap cost; pair it with
scoped()bindings to avoid state leakage. - Call
$kernel->terminate()in the worker loop to trigger request lifecycle cleanup. - Profile before optimising — JIT gains are CPU-bound; most Laravel apps are I/O-bound.