Framework runtime integration12 min read

Laravel Octane: persistent-worker serving for Laravel

What Laravel Octane is, how it keeps a Laravel app in memory on FrankenPHP, Open Swoole, Swoole or RoadRunner, and why state leakage matters.

PHP compatibility and project lifecycle

Supported PHP versions, Laravel versions, package release status and licence are not stated in the supplied source packet and are unknown. Confirm PHP 8.4 support in the official documentation before use. PHP 8.5 is planned for the track but unverified here.

Official sources checked 2026-10-22. Browser lessons on this site run PHP 8.5.

What this project is

Laravel Octane is a Laravel package, not a PHP interpreter and not a standalone runtime. Its documentation describes it as serving a Laravel application through an application server. The servers named are FrankenPHP, Open Swoole, Swoole and RoadRunner. Octane boots the application once, keeps it in memory, and feeds it requests. The classification here is framework runtime integration: the framework adapts itself to a long-lived server process, and the PHP language runtime underneath is still ordinary PHP.

Three distinctions help avoid confusion:

  • PHP core is the language engine. Octane is Laravel-specific code that sits above it. Laravel is a framework, not a CMS, so nothing here concerns Drupal or any other CMS product.
  • Swoole and Open Swoole are separate PHP extensions, installed with separate PECL package names. The documentation says Open Swoole grants the same functionality as Swoole, such as concurrent tasks, ticks and intervals.
  • FrankenPHP and RoadRunner are Go-based application servers. They are not PHP versions or forks.

Architecture and execution model

In a classic request lifetime, each request starts from a clean slate. The application is bootstrapped, handles one request, and its state is discarded. Octane replaces this with persistent workers. The application is loaded when the server starts, and the same in-memory application serves many requests.

The documentation describes several consequences:

  • Edits to files, including new routes in routes/web.php, are not picked up until the server restarts. A --watch flag can restart the server on file changes. It needs Node and the Chokidar library.
  • By default, Octane starts one request worker per CPU core. The table of contents lists options for worker count, maximum request count, maximum execution time, reloading workers and stopping the server.
  • Octane's own table of contents also lists sections on dependency injection, managing memory leaks, concurrent tasks, ticks and intervals, the Octane cache and tables. The supplied source packet is truncated before those sections, so their details are unknown here. Read them in the official documentation before relying on them.

State leakage and cleanup

With persistent workers, anything held in memory can outlive a single request. Static properties, singletons bound in the container, and objects that capture a request or configuration can carry data from one visitor to the next. The result can be stale data, cross-user data exposure, or memory growth. The documentation dedicates sections to dependency injection (container, request and configuration repository injection) and to managing memory leaks.

For your own code, a few habits follow from this model:

  • Treat static properties and long-lived singletons as shared across requests.
  • Do not store request-specific data in them. Prefer per-request objects.
  • Be careful when injecting the container, the request or the configuration repository into long-lived objects. Verify the official guidance first.
  • Use the worker request limit as a safety net, not as a substitute for fixing leaks.

The documentation's development Compose file runs with a max-requests value of 1, which restarts the worker after each request. That reduces leak and stale-code symptoms, so a leak that shows up under a persistent worker can be hidden in that setup. The example below deliberately omits that flag so you can see the effect.

Practical use cases

Octane suits Laravel applications where you want to avoid re-bootstrapping on every request, and where you can audit your code and packages for per-request state. It is a poor fit if you cannot review shared state, or if your hosting cannot run and supervise a long-lived process. Whether it improves speed for your workload is something to measure yourself. This guide makes no performance claims.

Installation approach

The documented path is to install the package with Composer and then run the octane:install Artisan command, which installs Octane's configuration file. You then choose a server:

  • FrankenPHP: with Octane, the binary can be downloaded for you. Official FrankenPHP Docker images are also documented for development and production, and they add platform support such as Windows.
  • RoadRunner: Octane offers to download the binary on first start. Under Sail, extra Composer packages and the rr executable are documented.
  • Swoole or Open Swoole: install the matching PHP extension, typically with PECL.

Start the server with the octane:start Artisan command. By default it listens on port 8000. The server comes from the server option in the octane configuration file. A custom Caddyfile can be supplied for FrankenPHP with a documented option.

Strengths

  • It integrates several documented server backends behind one Laravel command.
  • It offers FrankenPHP, Swoole and RoadRunner paths, with Laravel Sail instructions for each.
  • It provides built-in file watching for development.
  • It offers a clear production pattern: a process monitor plus a reverse proxy.

Limitations and unknowns

  • Supported PHP versions, Laravel version constraints, package release status and licence are not stated in the supplied source packet. They are unknown. The learning track uses PHP 8.4 with PHP 8.5 planned, so confirm compatibility before you try either.
  • The packet is truncated, so the memory-leak guidance, concurrency features and cache or table details are unverified here.
  • Persistent workers add real operational risk. Application and package code must be safe to run repeatedly in one process.
  • Lifecycle is marked unknown because the supplied evidence does not state a support policy.

Deployment

The documentation recommends a process monitor such as Supervisor to keep Octane running. It also recommends serving behind a traditional web server such as Nginx or Apache. That web server can serve static assets and handle certificate termination, and it proxies other requests to the Octane server on port 8000. When serving over HTTPS, the OCTANE_HTTPS environment variable controls whether generated links use https://. Keep the Octane port bound to localhost behind the proxy rather than exposed publicly. Laravel Cloud is mentioned in the source as offering managed Octane support. This guide does not cover provisioning.

Examples in this guide

The first example assumes an existing Laravel application. It adds a Docker setup adapted from the documentation, plus a tiny route that makes a leaky static counter visible. The second example shows the documented Supervisor and Nginx pairing. Both need Composer, Docker or a server, and network access. They are not browser exercises.

Links

The only source used is the official Laravel Octane documentation page, linked below.

Practical examples

These examples have not been run in a live environment yet. Commands and versions follow the official documentation linked on this page; check them before relying on them, and report a problem if one does not work.

FrankenPHP via Docker Compose with a visible static-state leak

Adapted from the Dockerfile and Compose file in the Octane documentation. It assumes an existing Laravel project root. Replace routes/web.php with the file shown and add the helper class. The counter route shows that a static property survives across requests inside a persistent worker. The documented development file uses a max-requests value of 1, which would hide this behaviour, so this file omits it.

Dockerfile

FROM dunglas/frankenphp

RUN install-php-extensions \
    pcntl
    # Add other PHP extensions here...

COPY . /app

ENTRYPOINT ["php", "artisan", "octane:frankenphp"]

compose.yaml

# compose.yaml
services:
  frankenphp:
    build:
      context: .
    entrypoint: php artisan octane:frankenphp --workers=1
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - .:/app

app/Support/RequestCounter.php

<?php

declare(strict_types=1);

namespace App\Support;

/**
 * Deliberately leaky: the static property lives as long as the worker process,
 * not as long as a single request.
 */
final class RequestCounter
{
    private static int $hits = 0;

    public static function hit(): int
    {
        self::$hits++;

        return self::$hits;
    }
}

routes/web.php

<?php

use App\Support\RequestCounter;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return 'Octane example';
});

Route::get('/counter', function () {
    return response()->json([
        'static_counter' => RequestCounter::hit(),
        'pid' => getmypid(),
    ]);
});

Install and run

composer require laravel/octane
php artisan octane:install --server=frankenphp
docker compose up --build
curl http://127.0.0.1:8000/counter
curl http://127.0.0.1:8000/counter

Expected behavior

If the container starts and is reachable, the first /counter response should show static_counter 1 and the second should show 2 with the same pid. That would indicate the static value persisted across requests in one worker. With the classic per-request model, or with a worker limit of 1 request, it would reset to 1.

Requires an existing Laravel application, Composer, Docker and network access. The Compose file uses the untagged image name from the documentation, so pin it yourself. Whether the server listens on an address reachable through the published port inside the container is not confirmed by the source and needs an integration check. PHP 8.4 support is unconfirmed. Code changes need a container restart. Not a browser exercise.

Supervisor and Nginx in front of Octane on a native host

Reproduces the documented production pattern: Supervisor keeps octane:start running on 127.0.0.1:8000, and Nginx serves static files and proxies other requests to it. The paths, user and domain are placeholders from the documentation. Adjust them for your own server.

deploy/octane.conf

[program:octane]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/example.com/artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/home/forge/example.com/storage/logs/octane.log
stopwaitsecs=3600

deploy/nginx-octane.conf

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name domain.com;
    server_tokens off;
    root /home/forge/domain.com/public;

    index index.php;

    charset utf-8;

    location /index.php {
        try_files /not_exists @octane;
    }

    location / {
        try_files $uri $uri/ @octane;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/domain.com-error.log error;

    error_page 404 /index.php;

    location @octane {
        set $suffix "";

        if ($uri = /index.php) {
            set $suffix ?$query_string;
        }

        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header SERVER_PORT $server_port;
        proxy_set_header REMOTE_ADDR $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_pass http://127.0.0.1:8000$suffix;
    }
}

Install and run

php artisan octane:start --server=frankenphp --host=127.0.0.1 --port=8000

Expected behavior

Octane listens on 127.0.0.1:8000 only. Nginx serves existing static files from the public directory and proxies other requests to Octane. Supervisor restarts the Octane process if it exits.

Placeholders for domain, user and paths must be replaced. The Supervisor and Nginx installation steps are not covered. Not a browser exercise. TLS setup is not shown. The OCTANE_HTTPS setting should be considered when serving over HTTPS.