Why Octane Changes Everything About Application State
Traditional PHP-FPM boots a fresh process per request. Laravel Octane — whether backed by Swoole, RoadRunner, or FrankenPHP — keeps a single worker alive and serves thousands of requests through it. Boot time drops dramatically, but the trade-off is brutal: anything bound as a singleton in the service container persists across requests unless you explicitly flush it.
Understanding the worker lifecycle is the first step to writing Octane-safe code.
The Worker Lifecycle in Three Phases
Boot phase → app()->boot(), all providers registered, singletons created
Request phase → clone of the app (sandbox) handles the request
Reset phase → sandbox discarded, base app state partially restored
Octane creates a sandbox — a shallow clone of the application container — for each request. Bindings marked as singletons in the base app are not re-instantiated; the same object is handed to every sandbox. That is the leakage vector.
Where State Leakage Actually Happens
1. Mutable Singleton Properties
class CartService
{
private array $items = [];
public function add(int $productId): void
{
$this->items[] = $productId;
}
public function all(): array
{
return $this->items;
}
}
Bound as a singleton, $items accumulates across every request in the same worker. User A's cart bleeds into User B's.
2. Static Properties
Static properties are not scoped to the container at all — they live on the class itself and survive every request, every sandbox, every flush.
class FeatureFlags
{
private static array $resolved = [];
// Never safe to cache per-request data here under Octane
}
3. Closures Capturing Request-Scoped Objects
A service provider that captures $request or Auth::user() inside a closure at boot time will freeze that value for the worker's lifetime.
The Octane Reset Hook
Octane fires Illuminate\Foundation\Events\RequestHandled and exposes a dedicated octane:request-handled hook. The cleaner API is the OctaneServiceProvider:
use Laravel\Octane\Facades\Octane;
class CartServiceProvider extends ServiceProvider
{
public function boot(): void
{
Octane::tick('flush-cart', function () {
// runs between requests — avoid heavy work here
});
$this->callAfterResolving(CartService::class, function (CartService $service) {
// not useful for reset; use the listener below
});
}
}
The correct approach is registering a listener on RequestHandled or using Octane's flush list:
// config/octane.php
'flush' => [
\App\Services\CartService::class,
],
Octane calls flush() on each listed class after every request. Implement the interface:
use Laravel\Octane\Contracts\ServesStaticFiles; // wrong example
// Octane checks for a flush() method via duck-typing
class CartService
{
private array $items = [];
public function flush(): void
{
$this->items = [];
}
}
Writing Genuinely Safe Singletons
The safest pattern is immutable singletons — services that hold only configuration or collaborators, never per-request data.
class PricingEngine
{
public function __construct(
private readonly TaxRateRepository $taxes,
private readonly CurrencyConverter $converter,
) {}
public function calculate(Money $amount, string $currency): Money
{
// Pure computation; no state stored on $this
return $this->converter->convert(
$amount->add($this->taxes->rateFor($currency)),
$currency,
);
}
}
When you genuinely need per-request state, bind it scoped rather than as a singleton:
$this->app->scoped(CartService::class, fn () => new CartService());
scoped() behaves like a singleton within a single request sandbox but is re-instantiated for the next request — exactly the semantics you want under Octane.
Detecting Leakage Before Production
Run your test suite with Octane's --workers=1 flag and fire the same endpoint twice in the same worker process. A simple integration test:
it('does not leak cart state between requests', function () {
$this->post('/cart', ['product_id' => 1]);
$this->post('/cart', ['product_id' => 2]);
$response = $this->getJson('/cart');
// Should contain only items from the current request session
$response->assertJsonCount(1, 'data');
});
Pair this with Octane's built-in octane:status command and memory profiling to catch workers that grow unboundedly.
Key Takeaways
- Octane sandboxes clone the container but share singleton instances from the base app.
- Mutable singleton properties and static class properties are the two most common leakage sources.
- Use
scoped()for per-request services; reservesingleton()for truly stateless collaborators. - Register a
flush()method and add the class toconfig/octane.php flushfor services that must reset. - Write immutable services by default — pure computation over stored state eliminates the problem entirely.