Laravel Octane with FrankenPHP: 10x Application Performance
·
7 min read
Introduction
Laravel Octane is an official Laravel package that supercharges your application by keeping it in memory between requests, instead of bootstrapping from scratch like traditional PHP-FPM.
FrankenPHP is the newest application server supported by Octane, built on Go and Caddy, delivering exceptional performance along with modern features like HTTP/3, Early Hints, and worker mode.
Why Octane?
In traditional PHP-FPM:
Request 1 → Bootstrap → Handle → Response → Destroy
Request 2 → Bootstrap → Handle → Response → Destroy
Request 3 → Bootstrap → Handle → Response → Destroy
With Octane:
Bootstrap (once)
↓
Request 1 → Handle → Response
Request 2 → Handle → Response
Request 3 → Handle → Response
Eliminating repeated bootstrapping significantly reduces response time.
Driver Comparison
Swoole
// Advantages
- Highest performance (benchmarks)
- Coroutines support
- WebSocket built-in
- Concurrent tasks
// Disadvantages
- Requires PHP extension
- Incompatible with some packages
- Harder to debug
RoadRunner
// Advantages
- No PHP extension required
- Easy installation
- Flexible plugin system
// Disadvantages
- Large binary size
- No coroutines
- Lower performance than Swoole
FrankenPHP
// Advantages
- Native HTTP/3 and Early Hints
- Automatic HTTPS
- Efficient worker mode
- Integrated Caddy server
- No extension required
- Docker-friendly
// Disadvantages
- Newer, smaller community
- Some edge cases not yet covered
Installing FrankenPHP
System Requirements
- PHP 8.2+
- Laravel 10+
- Docker (recommended)
Docker Installation
# Dockerfile
FROM dunglas/frankenphp:latest-php8.3
# Install required extensions
RUN install-php-extensions \
pcntl \
pdo_mysql \
redis \
opcache \
intl \
zip
# Copy application
COPY . /app
# Set working directory
WORKDIR /app
# Install composer dependencies
RUN composer install --no-dev --optimize-autoloader
# Configure Octane
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
Docker Compose
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "443:443"
- "80:80"
volumes:
- ./:/app
- caddy_data:/data
- caddy_config:/config
environment:
- APP_ENV=production
- OCTANE_SERVER=frankenphp
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
volumes:
- redis_data:/data
volumes:
caddy_data:
caddy_config:
mysql_data:
redis_data:
Installing Octane
# Install package
composer require laravel/octane
# Publish config
php artisan octane:install
# Select FrankenPHP when prompted
Octane Configuration
// config/octane.php
return [
'server' => env('OCTANE_SERVER', 'frankenphp'),
'https' => env('OCTANE_HTTPS', true),
'workers' => env('OCTANE_WORKERS', 'auto'),
'max_requests' => env('OCTANE_MAX_REQUESTS', 1000),
'listeners' => [
WorkerStarting::class => [
EnsureUploadedFilesAreValid::class,
EnsureUploadedFilesCanBeMoved::class,
],
RequestReceived::class => [
// Custom listeners
],
RequestHandled::class => [
// Cleanup after each request
],
RequestTerminated::class => [
FlushTemporaryContainerInstances::class,
],
],
// Warm these instances on worker start
'warm' => [
...Octane::defaultServicesToWarm(),
// Custom services
],
// Flush these instances between requests
'flush' => [
// Services that need reset
],
// Tables for concurrent state (Swoole only)
'tables' => [
'cache' => [
'columns' => [
['name' => 'value', 'type' => 'string', 'size' => 10000],
['name' => 'expiration', 'type' => 'int'],
],
'rows' => 1000,
],
],
];
Detailed FrankenPHP Configuration
Caddyfile
# Caddyfile
{
# Global options
frankenphp {
worker {
file ./public/index.php
num {$FRANKENPHP_WORKERS:auto}
env APP_ENV production
}
}
# Automatic HTTPS
auto_https on
# HTTP/3 support
servers {
protocols h1 h2 h3
}
}
:443, :80 {
root * /app/public
# Compression
encode zstd gzip
# Static file serving with cache
@static {
file
path *.css *.js *.ico *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2
}
handle @static {
header Cache-Control "public, max-age=31536000, immutable"
file_server
}
# Early Hints for critical resources
push /css/app.css
push /js/app.js
# PHP handling
php_server {
resolve_root_symlink
}
}
Environment Variables
# .env
OCTANE_SERVER=frankenphp
OCTANE_WORKERS=auto
OCTANE_MAX_REQUESTS=1000
OCTANE_HTTPS=true
# FrankenPHP specific
FRANKENPHP_WORKERS=auto
FRANKENPHP_CONFIG="worker ./public/index.php"
Performance Optimization
1. Service Warming
// app/Providers/OctaneServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Octane\Facades\Octane;
class OctaneServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Warm database connections
Octane::warm([
\Illuminate\Database\DatabaseManager::class,
]);
// Pre-compile views
Octane::tick('view-compiler', function () {
// Compile frequently used views
})->seconds(60);
}
}
2. Memory Management
// Flush services between requests
// config/octane.php
'flush' => [
// Reset auth guard
\Illuminate\Auth\AuthManager::class,
// Reset session
\Illuminate\Session\SessionManager::class,
// Custom services with state
\App\Services\CartService::class,
],
3. Avoiding Memory Leaks
// ❌ Wrong - Static property accumulates
class BadService
{
private static array $cache = [];
public function process($data): void
{
self::$cache[] = $data; // Memory leak!
}
}
// ✅ Correct - Reset after each request
class GoodService
{
private array $cache = [];
public function process($data): void
{
$this->cache[] = $data;
}
public function flush(): void
{
$this->cache = [];
}
}
// Register flush
Octane::onRequestTerminated(function () {
app(GoodService::class)->flush();
});
4. Concurrent Tasks
use Laravel\Octane\Facades\Octane;
// Run multiple tasks concurrently
[$users, $orders, $analytics] = Octane::concurrently([
fn () => User::active()->get(),
fn () => Order::recent()->get(),
fn () => Analytics::dashboard(),
]);
// With timeout
$results = Octane::concurrently([
'users' => fn () => User::all(),
'posts' => fn () => Post::published()->get(),
], 5000); // 5 seconds timeout
5. Smart Caching
// app/Services/CacheWarmer.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Laravel\Octane\Facades\Octane;
class CacheWarmer
{
public function warmOnWorkerStart(): void
{
// Cache config and routes
$this->warmConfig();
$this->warmRoutes();
$this->warmFrequentData();
}
private function warmConfig(): void
{
// Pre-load config into memory
config()->all();
}
private function warmRoutes(): void
{
// Pre-compile routes
app('router')->getRoutes()->refreshNameLookups();
}
private function warmFrequentData(): void
{
// Cache frequently accessed data
Cache::remember('settings', 3600, fn () =>
Setting::all()->pluck('value', 'key')
);
Cache::remember('categories', 3600, fn () =>
Category::with('children')->whereNull('parent_id')->get()
);
}
}
Real-World Benchmarks
Test Setup
# Install wrk
apt install wrk
# Test endpoint
wrk -t12 -c400 -d30s http://localhost/api/benchmark
Comparison Results
| Metric | PHP-FPM | RoadRunner | Swoole | FrankenPHP |
|---|---|---|---|---|
| Requests/sec | 850 | 3,200 | 4,500 | 4,100 |
| Latency (avg) | 45ms | 12ms | 8ms | 9ms |
| Memory | 512MB | 256MB | 280MB | 240MB |
| CPU Usage | 85% | 60% | 55% | 58% |
Benchmark Script
// routes/api.php
Route::get('/benchmark', function () {
// Simulate database query
$users = User::limit(10)->get();
// Simulate cache hit
$settings = Cache::remember('bench_settings', 60, fn () => [
'app_name' => config('app.name'),
'timestamp' => now(),
]);
// Simulate JSON response
return response()->json([
'users' => $users,
'settings' => $settings,
'memory' => memory_get_usage(true),
]);
});
HTTP/3 and Early Hints
HTTP/3 Configuration
{
servers {
protocols h1 h2 h3
}
}
:443 {
# Enable HTTP/3
header Alt-Svc `h3=":443"; ma=86400`
# Your config...
}
Early Hints
// app/Http/Middleware/EarlyHints.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EarlyHints
{
public function handle(Request $request, Closure $next)
{
// Send Early Hints for critical resources
if ($request->expectsHtml()) {
$this->sendEarlyHints([
'</css/app.css>; rel=preload; as=style',
'</js/app.js>; rel=preload; as=script',
'</fonts/inter.woff2>; rel=preload; as=font; crossorigin',
]);
}
return $next($request);
}
private function sendEarlyHints(array $links): void
{
if (function_exists('headers_send_early')) {
headers_send_early(['Link' => implode(', ', $links)]);
}
}
}
Troubleshooting Common Issues
1. Memory Limit
// Increase memory limit
ini_set('memory_limit', '512M');
// Or in octane.php
'max_requests' => 500, // Restart worker after 500 requests
2. Database Connection Lost
// app/Listeners/RefreshDatabaseConnection.php
namespace App\Listeners;
use Illuminate\Support\Facades\DB;
class RefreshDatabaseConnection
{
public function handle(): void
{
// Reconnect if connection lost
try {
DB::connection()->getPdo();
} catch (\Exception $e) {
DB::reconnect();
}
}
}
3. Session Issues
// Use database or redis session
SESSION_DRIVER=redis
// config/session.php
'driver' => env('SESSION_DRIVER', 'redis'),
'connection' => 'session',
Production Deployment
Health Check
// routes/web.php
Route::get('/health', function () {
return response()->json([
'status' => 'healthy',
'octane' => true,
'server' => config('octane.server'),
'workers' => env('OCTANE_WORKERS', 'auto'),
]);
});
Graceful Shutdown
# Restart workers gracefully
php artisan octane:reload
# Stop server
php artisan octane:stop
Monitoring
// Log performance metrics
Octane::tick('metrics', function () {
Log::channel('metrics')->info('Worker stats', [
'memory' => memory_get_usage(true),
'peak_memory' => memory_get_peak_usage(true),
'requests_handled' => app('octane.request_count') ?? 0,
]);
})->seconds(30);
Conclusion
FrankenPHP is an excellent choice for Laravel Octane with:
- Easy installation: No PHP extension required
- Modern: HTTP/3, Early Hints, automatic HTTPS
- High performance: Nearly matches Swoole
- Docker-friendly: Single binary, easy to deploy
Recommendations:
- Development: FrankenPHP (easy setup)
- Small-medium production: FrankenPHP
- Large production needing WebSocket: Swoole
- No extension preference: RoadRunner or FrankenPHP