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

References

Comments