Octane and Long-Lived Workers
Octane boots the application once and keeps it in memory, which removes per-request bootstrap and exposes leaking state.
php artisan octane:start --workers=4 --max-requests=500Under PHP-FPM every request builds the framework from nothing: autoloading, service providers, config, routes. Then the process dies and the next request does it again. Laravel Octane replaces that model with long-lived workers. The application is booted once per worker, kept in memory, and fed requests.
Install it with composer require laravel/octane and php artisan octane:install, which writes config/octane.php and asks which server to use: FrankenPHP, Swoole, Open Swoole or RoadRunner. php artisan octane:start serves on port 8000 with one request worker per CPU core.
What changes
A service provider's register and boot run once per worker, not once per request. Octane resets first-party framework state between requests. It cannot reset yours.
What leaks
Two things. A singleton that took the container, the request or the config repository in its constructor keeps that first one forever, so later requests read stale input. Inject a resolver closure instead: fn () => Container::getInstance() is the short form of function () { return Container::getInstance(); }, and the request() and config() helpers always return the current ones. Second, anything appended to a static property grows for the life of the worker. Octane restarts a worker after 500 requests, adjustable with --max-requests, which limits the damage rather than excusing it.
Example
<?php
// app/Providers/AppServiceProvider.php
declare(strict_types=1);
namespace App\Providers;
use App\Services\FeatureFlags;
use Illuminate\Container\Container;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// This closure runs once per worker, not once per request. Handing
// the service $app->make('config') here would freeze the repository
// it saw at boot; a resolver closure fetches the live one on every
// call, which is what Container::getInstance() returns.
$this->app->singleton(FeatureFlags::class, function (): FeatureFlags {
return new FeatureFlags(fn () => Container::getInstance()->make('config'));
});
}
}When to use it
- Cutting framework bootstrap out of a high-traffic JSON API.
- Auditing singletons and static state before switching a running app to Octane.
- Tracking down a service that answers with the previous request's input.
- Reloading workers as the last step of a deploy.
More examples
Running and reloading the server
In production, run octane:start under a process monitor such as Supervisor, behind Nginx or Apache so the web server handles static files and TLS, and finish every deploy with octane:reload. Omitting --workers starts one worker per CPU core. --task-workers applies to Swoole only, where it sizes the pool used by Octane::concurrently().
composer require laravel/octane
# Writes config/octane.php and asks which server to use.
php artisan octane:install
# Four request workers, each recycled after 500 requests.
php artisan octane:start --workers=4 --max-requests=500
# Development: restart on file changes (needs node and chokidar).
php artisan octane:start --watch
# Deployment: load the new code into the running workers.
php artisan octane:reload
php artisan octane:status
php artisan octane:stopThe leak that PHP-FPM hid
Static arrays, static caches keyed by model id, and hand-rolled in-memory memoisation all behave this way: correct under FPM, a slow leak under Octane. If a value is needed for the duration of one request, keep it on the request, in a request-scoped container binding ($this->app->scoped(...), which Octane flushes between requests), or in the cache. Watching a worker's memory climb during local development is how you find these before production does.
<?php
// app/Http/Controllers/ReportController.php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Report;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ReportController extends Controller
{
/**
* Under PHP-FPM this array died with the process. Under Octane it grows
* for the life of the worker: one entry per request, never released.
*/
public static array $rendered = [];
public function show(Request $request, Report $report): View
{
self::$rendered[] = $report->id;
return view('reports.show', ['report' => $report]);
}
}Three bindings, two of them safe
This is the single most common Octane bug, and it presents as one user seeing another user's data. The symptom is confusing because the first request after a worker boots is always correct. Type-hinting Illuminate\Http\Request on a controller method or route closure stays safe - the container resolves it per request - and so does the request() helper anywhere in your code. It is constructor injection into something long-lived that freezes.
<?php
// app/Providers/AppServiceProvider.php - register()
use App\Services\Tenant;
use Illuminate\Contracts\Foundation\Application;
// Broken under Octane: resolved once, holding the first request forever.
$this->app->singleton(Tenant::class, function (Application $app) {
return new Tenant($app['request']);
});
// Safe: a new instance per resolution, so a new request each time.
$this->app->bind(Tenant::class, function (Application $app) {
return new Tenant($app['request']);
});
// Safe: one instance, but it resolves the current request when it needs it.
$this->app->singleton(Tenant::class, function (Application $app) {
return new Tenant(fn () => $app['request']);
});
Discussion