Laravel Octane in 2026: Your App Is Probably 10× Slower Than It Needs to Be

Most Laravel apps restart the framework on every single request. Octane eliminates that — and the performance difference isn’t subtle. Here’s how to get sub-10ms response times with FrankenPHP, Swoole, and RoadRunner.


Every request to a standard Laravel application on PHP-FPM goes through the same ritual. PHP spins up. Autoloading runs. Every service provider boots. The IoC container is built from scratch. The router is compiled. Config and routes are cached into memory. The request is handled. PHP dies. Repeat.

For a typical Laravel application, this boot process takes 10–30ms depending on the number of service providers and the complexity of the application. This overhead is invisible to users on fast servers, but at high concurrency it becomes a significant fraction of total processing time.

Laravel Octane eliminates that ritual. Octane boots your application once, keeps it in memory, and then feeds subsequent requests to already-booted workers — removing repeated bootstrap overhead and yielding much lower latency and higher requests-per-second.

The performance difference isn’t subtle. In a July 2025 benchmark of Laravel 12, FrankenPHP handled nearly 5× more requests per second than PHP-FPM and still outpaced RoadRunner or the Swoole variants. On simple API routes, response times drop from 20–40ms to under 5ms. On complex applications with many service providers, the improvement is even more dramatic.

This guide covers everything: the architecture, all three drivers, safe adoption patterns, the state management pitfalls that will break your app, concurrent tasks with Swoole, and deploying Octane in production with zero downtime.


The Architecture Difference

PHP-FPM (FastCGI Process Manager) manages a pool of worker processes. When Nginx receives a PHP request, it forwards the request to an available FPM worker via a Unix socket or TCP connection. The framework boot process happens on every single request.

PHP-FPM Model (Traditional)

Request 1 → [Boot Laravel] → [Handle] → [Die]
Request 2 → [Boot Laravel] → [Handle] → [Die]
Request 3 → [Boot Laravel] → [Handle] → [Die]
             ↑ 10-30ms wasted on every single request

Octane’s model is fundamentally different:

Octane Model (Persistent Workers)

[Boot Laravel Once]
       │
       ├─ Worker 1 → Request 1 → Request 4 → Request 7 …
       ├─ Worker 2 → Request 2 → Request 5 → Request 8 …
       └─ Worker 3 → Request 3 → Request 6 → Request 9 …
             ↑ Framework is already warm — zero boot overhead

The framework boots once per worker, and each worker handles hundreds or thousands of requests before being recycled. The implications are enormous:

  • Service providers register once, not per request
  • Configuration is parsed once, not per request
  • Routes are compiled once, not per request
  • Database connection pools are maintained, not recreated
  • Class instances can be cached in memory across requests

Choosing a Driver: FrankenPHP, Swoole, or RoadRunner

Laravel Octane supercharges your application’s performance by serving your application using high-powered application servers, including FrankenPHP, Open Swoole, Swoole, and RoadRunner.

Each driver has different architecture, trade-offs, and operational requirements.

FrankenPHP

FrankenPHP is the newest of the three drivers and has gained rapid adoption due to its ease of deployment. It is a PHP application server written in Go, built on top of Caddy, that embeds PHP directly into the server binary. No Nginx. No separate proxy. No PHP extension to compile. A single binary runs your entire application.

For CPU-bound or framework-heavy workloads, FrankenPHP’s worker mode provides similar performance gains to other drivers by eliminating bootstrap overhead.

Best for: Teams that want Octane performance with the simplest possible setup. Local development. Containerised deployments where a single binary is ideal. Teams that don’t want to manage PHP extensions.

Operational profile:

  • Single binary — no Nginx required
  • Built-in HTTPS with automatic certificate management
  • Easiest to Dockerise
  • Worker mode keeps Laravel booted in memory
FrankenPHP Architecture

Internet → FrankenPHP (Go binary + embedded PHP)
                │
                └── Worker 1 (booted Laravel)
                └── Worker 2 (booted Laravel)
                └── Worker N (booted Laravel)

Swoole

Swoole takes a fundamentally different approach. Rather than bolting a persistent worker onto PHP’s traditional execution model, Swoole extends PHP itself with an asynchronous, event-driven runtime — similar to what Node.js provides for JavaScript. A single Swoole worker can handle thousands of concurrent connections using cooperative multitasking.

Swoole is designed based on the principles of Erlang, Node.js, and Netty, but specifically for PHP. However, since Swoole is only compatible with the Linux Kernel, it currently only works on Linux, OS X, Cygwin, or WSL.

Swoole also unlocks features that no other driver supports: native coroutines for concurrent I/O, task workers for background processing, and Swoole Tables — in-memory shared data structures accessible by all workers.

Best for: Applications with many concurrent external API calls, parallel database queries, or I/O-heavy workloads where coroutines deliver the most benefit. Teams comfortable with PHP extensions.

Swoole Architecture

Internet → Nginx (SSL, static files)
               │
               └── Swoole HTTP Server (PHP extension)
                       │
                       ├── Worker 1 (coroutine pool)
                       ├── Worker 2 (coroutine pool)
                       └── Task Workers (background tasks)

RoadRunner

RoadRunner is a high-performance PHP application server written in Go. Like FrankenPHP’s worker mode, it keeps your PHP application loaded in memory, but RoadRunner has been doing this since 2018 and has a more mature plugin ecosystem. RoadRunner manages a pool of long-lived PHP worker processes. Each worker loads your application once and handles many requests sequentially. The Go layer handles HTTP, load balancing, and additional services.

Best for: Teams that prefer not to install PHP extensions and want a Go-based process manager with broad feature support. Enterprise environments. Applications that need RoadRunner’s plugin ecosystem (metrics, KV store, gRPC).

RoadRunner Architecture

Internet → Nginx (SSL, static files)
               │
               └── RoadRunner (Go binary)
                       │
                       ├── PHP Worker 1 (booted Laravel)
                       ├── PHP Worker 2 (booted Laravel)
                       └── PHP Worker N (booted Laravel)

Driver Comparison

FeatureFrankenPHPSwooleRoadRunner
Extension requiredNoYes (PECL)No
Nginx requiredNoYesYes
CoroutinesNoYesNo
Task workersNoYesNo
Swoole TablesNoYesNo
Ease of setupEasiestModerateModerate
Docker-friendlinessExcellentGoodGood
Plugin ecosystemCaddyN/AExcellent
Production maturityGrowing fastBattle-testedBattle-tested
Best workloadGeneral purposeI/O-heavyGeneral purpose

There’s no single answer. For I/O-bound workloads with many database queries, Swoole’s coroutine model typically achieves the highest throughput. For CPU-bound or framework-heavy workloads, FrankenPHP and RoadRunner’s worker modes provide similar performance gains by eliminating bootstrap overhead. Always benchmark with your actual application rather than relying on synthetic tests.


Installation and Setup

Installing Octane

composer require laravel/octane

php artisan octane:install
# Choose your driver: frankenphp, swoole, or roadrunner

FrankenPHP Setup

# Install with FrankenPHP (downloads the binary automatically)
php artisan octane:install --server=frankenphp

# Start the server
php artisan octane:start --server=frankenphp

# With custom workers and port
php artisan octane:start --server=frankenphp --workers=auto --port=8000
# Dockerfile for FrankenPHP — single binary, no Nginx needed
FROM dunglas/frankenphp:latest-php8.3

WORKDIR /app
COPY . .

RUN composer install --no-dev --optimize-autoloader
RUN php artisan config:cache
RUN php artisan route:cache
RUN php artisan view:cache

CMD ["php", "artisan", "octane:start", "--server=frankenphp", \
     "--host=0.0.0.0", "--port=8080", "--workers=auto"]

Swoole Setup

# Install the Swoole PHP extension
pecl install swoole

# Add to php.ini
echo "extension=swoole" >> /etc/php/8.3/cli/php.ini

# Install Octane with Swoole
php artisan octane:install --server=swoole

# Start with workers and task workers
php artisan octane:start --server=swoole \
  --workers=8 \
  --task-workers=4 \
  --max-requests=500

RoadRunner Setup

# Install Octane with RoadRunner (downloads the binary automatically)
php artisan octane:install --server=roadrunner

# Start the server
php artisan octane:start --server=roadrunner --workers=8

config/octane.php — Key Configuration

// config/octane.php
return [
    // The server to use: 'frankenphp', 'swoole', 'roadrunner'
    'server' => env('OCTANE_SERVER', 'frankenphp'),

    // Number of workers — 'auto' uses CPU count
    'workers' => env('OCTANE_WORKERS', 'auto'),

    // Requests per worker before recycling (memory leak defense)
    'max_requests' => env('OCTANE_MAX_REQUESTS', 500),

    // Swoole-specific configuration
    'swoole' => [
        'options' => [
            'task_worker_num'      => env('OCTANE_TASK_WORKERS', 4),
            'dispatch_mode'        => 2,
            'max_coroutine'        => 3000,
            'max_wait_time'        => 30,
            'package_max_length'   => 10 * 1024 * 1024, // 10MB
        ],
    ],

    // Warm-up: classes to pre-load when a worker boots
    'warm' => [
        ...Octane::defaultServicesToWarm(),
        App\Services\PricingEngine::class,
        App\Services\RecommendationService::class,
    ],

    // Flush: listeners to reset state between requests
    'flush' => [
        // Add any custom singletons that need resetting
    ],

    // Tables: in-memory shared data (Swoole only)
    'tables' => [
        'example' => [
            'size'    => 1000,
            'columns' => [
                ['name' => 'name',  'type' => Table::TYPE_STRING, 'size' => 1000],
                ['name' => 'value', 'type' => Table::TYPE_INT,    'size' => 4],
            ],
        ],
    ],
];

The Critical Danger: State Management

This is where most Octane migrations go wrong. In a traditional PHP-FPM application, every request gets a clean slate — any state you set is wiped when the process dies. In Octane, workers are long-lived. State you set in one request will still be there in the next request, served to a completely different user.

This is not theoretical. It causes real bugs: users seeing each other’s data, authentication state leaking between requests, configuration changes not taking effect.

The Static Property Problem

// ✗ DANGEROUS — this leaks between requests in Octane
class CartService
{
    private static array $items = [];  // static state persists across requests!

    public static function addItem(string $sku): void
    {
        self::$items[] = $sku;
    }

    public static function getItems(): array
    {
        return self::$items;  // next request still sees previous request's items
    }
}

// ✓ SAFE — instance state, not static state
class CartService
{
    private array $items = [];  // instance property, safe

    public function addItem(string $sku): void
    {
        $this->items[] = $sku;
    }

    public function getItems(): array
    {
        return $this->items;
    }
}

The Singleton Problem

Singletons registered in the container are also long-lived. If a singleton stores request-specific state, it will leak.

// ✗ DANGEROUS — singleton that stores request-specific data
class CurrentUser
{
    private ?User $user = null;

    public function set(User $user): void
    {
        $this->user = $user;  // persists to the next request!
    }

    public function get(): ?User
    {
        return $this->user;
    }
}

// ✓ SAFE — request-scoped binding (recreated every request)
// In a ServiceProvider:
public function register(): void
{
    $this->app->scoped(CurrentUser::class, function () {
        return new CurrentUser();
    });
}
// scoped() bindings are automatically reset between requests by Octane

Using scoped() — The Correct Pattern for Request State

scoped() bindings are one of Octane’s most important tools. They behave like singleton() within a single request, but Octane resets them between requests.

// ServiceProvider
public function register(): void
{
    // This is recreated fresh for every request
    $this->app->scoped(AuthContext::class, function ($app) {
        return new AuthContext();
    });

    $this->app->scoped(RequestLocale::class, function ($app) {
        return new RequestLocale(request()->header('Accept-Language'));
    });
}

Detecting Leaks with Octane’s RequestHandled Listener

// app/Providers/OctaneServiceProvider.php
use Laravel\Octane\Events\RequestHandled;

public function boot(): void
{
    // Reset custom state after every request
    Event::listen(RequestHandled::class, function ($event) {
        // Flush any static properties or custom state here
        SomeService::reset();
        app(SomeStatefulClass::class)->flush();
    });
}

The –max-requests Safety Net

Even with careful state management, memory can creep upward over thousands of requests. The --max-requests option recycles workers after a fixed number of requests, starting fresh.

# Workers restart after handling 500 requests
php artisan octane:start --max-requests=500

Keeping a sane --max-requests is the simplest defence against slow, creeping memory leaks. Set it. Then monitor memory usage and adjust based on your application’s profile.


Concurrent Tasks with Swoole

Swoole’s task workers unlock a pattern unavailable in standard PHP: true concurrent execution within a single request. Instead of waiting for three API calls sequentially, you dispatch them all simultaneously and collect results.

use Laravel\Octane\Facades\Octane;

// ✗ Sequential — 3 API calls × 200ms each = 600ms total
public function dashboard(): array
{
    $user        = $this->fetchUser();         // 200ms
    $orders      = $this->fetchOrders();       // 200ms
    $analytics   = $this->fetchAnalytics();    // 200ms
    // Total: 600ms
    return compact('user', 'orders', 'analytics');
}

// ✓ Concurrent — 3 API calls running simultaneously = ~200ms total
public function dashboard(): array
{
    [$user, $orders, $analytics] = Octane::concurrently([
        fn () => $this->fetchUser(),       // ─┐
        fn () => $this->fetchOrders(),     //  ├── all run in parallel
        fn () => $this->fetchAnalytics(),  // ─┘
    ]);
    // Total: ~200ms (longest single call)

    return compact('user', 'orders', 'analytics');
}

Concurrent Database Queries

use Laravel\Octane\Facades\Octane;

public function salesReport(int $year): array
{
    [$monthly, $topProducts, $topRegions, $refundRate] = Octane::concurrently([
        fn () => Order::whereYear('created_at', $year)
                      ->selectRaw('MONTH(created_at) as month, SUM(total) as revenue')
                      ->groupBy('month')
                      ->get(),

        fn () => OrderItem::join('products', 'products.id', '=', 'order_items.product_id')
                          ->whereYear('orders.created_at', $year)
                          ->selectRaw('products.name, SUM(order_items.quantity) as units_sold')
                          ->orderByDesc('units_sold')
                          ->limit(10)
                          ->get(),

        fn () => Order::whereYear('created_at', $year)
                      ->selectRaw('region, SUM(total) as revenue')
                      ->groupBy('region')
                      ->orderByDesc('revenue')
                      ->get(),

        fn () => $this->calculateRefundRate($year),
    ]);

    return compact('monthly', 'topProducts', 'topRegions', 'refundRate');
}

Octane Tick — Periodic In-Process Tasks

With Swoole, you can run callbacks at a fixed interval inside the Octane server process — without a separate scheduler.

// In OctaneServiceProvider or AppServiceProvider
use Laravel\Octane\Facades\Octane;

public function boot(): void
{
    // Runs every 5 seconds in every Swoole worker
    Octane::tick('heartbeat', function () {
        Cache::put('server:heartbeat', now(), seconds: 30);
    })->seconds(5);

    // Warm up a price cache every 60 seconds
    Octane::tick('price-cache', function () {
        app(PricingEngine::class)->warmCache();
    })->seconds(60);
}

Swoole Tables: In-Memory Shared Data

Swoole Tables are fixed-size, in-memory hash tables that all workers can read and write simultaneously — with no database round-trip.

// Define in config/octane.php
'tables' => [
    'rate_limits' => [
        'size'    => 10000,
        'columns' => [
            ['name' => 'key',       'type' => Table::TYPE_STRING, 'size' => 100],
            ['name' => 'hits',      'type' => Table::TYPE_INT,    'size' => 4],
            ['name' => 'reset_at',  'type' => Table::TYPE_INT,    'size' => 8],
        ],
    ],
],
// Using a Swoole Table for ultra-fast rate limiting
use Laravel\Octane\Facades\Octane;

class RateLimiter
{
    public function check(string $key, int $limit, int $windowSeconds): bool
    {
        $table    = Octane::table('rate_limits');
        $now      = time();
        $entry    = $table->get($key);

        if (!$entry || $entry['reset_at'] < $now) {
            // Window expired or first request — reset counter
            $table->set($key, ['key' => $key, 'hits' => 1, 'reset_at' => $now + $windowSeconds]);
            return true;
        }

        if ($entry['hits'] >= $limit) {
            return false;  // rate limit exceeded
        }

        $table->incr($key, 'hits', 1);
        return true;
    }
}

Nginx Configuration for Octane

When using Swoole or RoadRunner (which still sit behind Nginx), the Nginx configuration differs from a standard PHP-FPM setup:

# /etc/nginx/sites-available/your-app

server {
    listen 80;
    listen [::]:80;
    server_name your-app.com;
    root /var/www/your-app/public;

    # Static files served directly by Nginx — never hits Octane
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff|woff2|svg|webp)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # All dynamic requests proxied to Octane
    location / {
        proxy_pass              http://127.0.0.1:8000;
        proxy_http_version      1.1;
        proxy_set_header        Host              $host;
        proxy_set_header        X-Real-IP         $remote_addr;
        proxy_set_header        X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;
        proxy_set_header        Upgrade           $http_upgrade;
        proxy_set_header        Connection        "upgrade";
        proxy_read_timeout      60s;
        proxy_connect_timeout   60s;
    }
}

Deploying Octane in Production

Supervisor Configuration

; /etc/supervisor/conf.d/octane.conf

[program:octane]

process_name=%(program_name)s command=php /var/www/your-app/artisan octane:start \ –server=frankenphp \ –port=8080 \ –workers=auto \ –max-requests=500 \ –no-interaction autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/www/your-app/storage/logs/octane.log stopwaitsecs=30 stopsignal=SIGTERM

Zero-Downtime Deployment

Unlike PHP-FPM, Octane keeps your application in memory. When you deploy new code, you must reload the workers. Octane provides a graceful reload command that finishes in-flight requests before restarting workers.

# During deployment — after running composer install, migrations, etc.

# Reload workers gracefully (finishes current requests, then restarts with new code)
php artisan octane:reload

# Or if using Supervisor to manage Octane:
supervisorctl reload octane
# Full deployment script example
#!/bin/bash

# Switch to new release
php artisan down --render="maintenance"

composer install --no-dev --optimize-autoloader

php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

php artisan up

# Reload Octane workers with new code
php artisan octane:reload

Health Check Endpoint

// routes/web.php — or use a dedicated health check package
Route::get('/health', function () {
    return response()->json([
        'status'    => 'ok',
        'timestamp' => now()->toISOString(),
        'workers'   => config('octane.workers'),
    ]);
})->name('health');

Octane Artisan Commands Reference

# ── Starting and Managing ──────────────────────────────────────────
# Start the Octane server
php artisan octane:start

# Start with specific driver, workers, and port
php artisan octane:start --server=swoole --workers=8 --port=8000

# Start in watch mode (restarts on file changes — for development)
php artisan octane:start --watch

# Stop the server gracefully
php artisan octane:stop

# Reload workers gracefully (zero-downtime deploy)
php artisan octane:reload

# Check server status
php artisan octane:status


# ── Diagnostics ───────────────────────────────────────────────────
# See which workers are running and their request counts
php artisan octane:status

# Monitor memory usage per worker
php artisan octane:status --memory

Performance Optimisations to Stack with Octane

Octane eliminates bootstrap overhead — but combine it with these optimisations for the full performance picture.

1. OPcache with Preloading

; php.ini — OPcache configuration for Octane
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0       ; disable in production — requires opcache reset on deploy
opcache.revalidate_freq=0
opcache.preload=/var/www/your-app/vendor/laravel/octane/src/preload.php
opcache.preload_user=www-data

2. Cache All The Things

# Run these on every deploy — Octane loads from cache on boot
php artisan config:cache    # merges all config into one file
php artisan route:cache     # compiles routes into a fast lookup
php artisan view:cache      # pre-compiles Blade templates
php artisan event:cache     # caches event-listener mappings

3. Reduce Service Providers

Every service provider adds to boot time. Octane boots once, but unnecessary providers still waste memory per worker.

// config/app.php — audit and remove unused providers
'providers' => [
    // Only include providers your application actually uses
    // Comment out anything from packages you don't actively use
],

4. Eager Load Relationships

Under Octane, N+1 queries are even more wasteful because worker memory is precious. Use with() consistently.

// ✗ N+1 — 1 + N queries per request
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->user->name;  // query per iteration
}

// ✓ Eager loaded — 2 queries total
$orders = Order::with('user', 'items.product')->get();

When NOT to Use Octane

Octane is not universally the right tool. Be honest about these constraints before adopting it.

Don’t use Octane if:

  • Your application uses packages with static state that you can’t safely audit. Some older packages store request-scoped data in static properties. These will leak between requests and are difficult to fix without forking the package.
  • You run on shared hosting. Octane requires a long-running process, which shared hosting providers do not support.
  • Your application is I/O bound by the database and adding more replicas is cheaper than adopting Octane. The performance gains are real but adding a read replica might give you more practical throughput for less engineering effort.
  • Your team doesn’t have the operational capacity to manage Octane’s deployment requirements. Reloading workers on deploy, monitoring memory, and managing the Supervisor process is additional operational surface area.

The honest benchmark caveat: Octane is not a drop-in replacement for PHP-FPM. It introduces new constraints, requires attention to memory management, and is not compatible with all Laravel packages. Benchmark your actual application — synthetic benchmarks on simple routes will always show dramatic gains, but your real-world improvement depends heavily on your application’s complexity and workload characteristics.


Migration Checklist

Before switching from PHP-FPM to Octane in production:

✓ Audit all static properties — request-scoped statics will leak
✓ Replace singleton() with scoped() for request-scoped state
✓ Add Octane RequestHandled listener to flush custom state
✓ Set --max-requests (500 is a reasonable starting point)
✓ Test with OCTANE_SERVER=roadrunner or =frankenphp in staging first
✓ Update Supervisor config for octane:start instead of queue:work
✓ Update deploy script to run octane:reload after code changes
✓ Update Nginx config to proxy to Octane port instead of PHP-FPM socket
✓ Enable OPcache with validated=0 and preloading
✓ Run config:cache, route:cache, view:cache, event:cache on deploy
✓ Add /health endpoint for load balancer health checks
✓ Set up memory monitoring — alert if worker memory grows steadily
✓ Test file uploads — Octane handles multipart differently
✓ Verify session and cookie handling (middleware order matters)
✓ Run php artisan octane:start --watch in development for DX

Final Thoughts

The framework boot process in a typical Laravel application takes 10–30ms per request. At high concurrency, this becomes a significant fraction of total processing time. Eliminating it with Octane is one of the highest-leverage performance improvements available to a Laravel application — requiring zero changes to your business logic.

Laravel Octane turns PHP’s traditional request-per-process model on its head. By keeping your app hot in memory and leveraging modern servers such as FrankenPHP, you unlock performance previously reserved for Go or Node stacks. The key is to respect the new rules of a persistent runtime: guard against shared state, recycle workers, and profile memory. Do that, and you’ll enjoy API responses that feel instant, happier users, and infrastructure that can handle high-traffic events without breaking a sweat.

Start with FrankenPHP. It requires no PHP extensions, has the simplest deployment story, and delivers comparable performance to Swoole for most workloads. If your application is I/O-bound with many concurrent external API calls, Swoole’s coroutine model and concurrent tasks will give you additional headroom.

The migration is an afternoon of work. The performance improvement lasts forever.

Leave a Reply

Your email address will not be published. Required fields are marked *