In Laravel Octane, how do you reset your own static state before each request through the listeners config, and which events can you hook?
answer
- octane.listeners maps events to classes
- RequestReceived: app, sandbox, request
- prepareApplicationForNextRequest spread
- OperationTerminated after request, task, tick
- static array grows with each request
basics
~20 sAdd a listener class under RequestReceived in the listeners array of config/octane.php, after Octane's own spread entries; its handle($event) runs before each request and can reset statics. Other hooks include RequestTerminated, OperationTerminated, WorkerStarting and WorkerStopping.
solid answer
~40 s`config/octane.php` has a `listeners` array mapping Octane event classes to listener classes; the service provider registers each with Laravel's event dispatcher. `RequestReceived` already spreads `...Octane::prepareApplicationForNextOperation()` and `...Octane::prepareApplicationForNextRequest()` - Octane's own reset listeners - and you append yours below them. The event carries `$event->app` (the base application), `$event->sandbox` (this request's clone) and `$event->request`, so a listener can clear a static cache or re-point a service at the sandbox. Other hooks: `RequestHandled`, `RequestTerminated`, the `OperationTerminated` contract (after any request, task or tick), `TaskReceived`/`TickReceived`, `WorkerStarting`, `WorkerStopping` and `WorkerErrorOccurred`. A static array that grows per request is both a data and a memory leak; reset it or better, remove it.
code
php · 15 lines<?php
namespace App\Listeners;
use App\Support\PermissionCache;
use Laravel\Octane\Events\RequestReceived;
class ResetPermissionCache
{
public function handle(RequestReceived $event): void
{
// Static cache filled per user; must not survive the request
PermissionCache::$byUser = [];
}
}go deeper
Recall that static state survives between requests under Octane and that config/octane.php's listeners array is where you add a reset.
Explain the RequestReceived event's app, sandbox and request properties, the spread default listeners, and which other events exist.
Show you choose reset points defensively, keep listeners cheap, and prefer removing statics or scoped bindings over growing a list of resets.
Decide when a codebase's reliance on static state is a design problem to retire rather than a list of Octane listeners to maintain.
## Why you need a hook Under **Laravel Octane**, a static property on your own class lives as long as the worker process. Octane's docs show the textbook case: a controller that does `Service::$data[] = Str::random(10);` on every request. Under PHP-FPM that array dies with the request; under Octane it grows forever, a **memory leak**, and if it holds request data, a **data leak**. The best fix is to remove the static. When you cannot - legacy code, a package, a deliberate per-request registry - reset it at a known point with an Octane listener. ## The `listeners` array `config/octane.php` maps Octane's events to listener classes: ```php 'listeners' => [ RequestReceived::class => [ ...Octane::prepareApplicationForNextOperation(), ...Octane::prepareApplicationForNextRequest(), // ], OperationTerminated::class => [ FlushOnce::class, FlushTemporaryContainerInstances::class, // DisconnectFromDatabases::class, // CollectGarbage::class, ], ], ``` Octane's service provider reads this array and registers each entry with Laravel's event dispatcher, so a listener is an ordinary class with a `handle($event)` method, resolved from the container. Keep the two spread calls: they are **Octane's own resets** - cloning config and the URL generator, giving the new application instance to the database, cache, session, queue, mail, log and view managers, flushing authentication state, the locale, queued cookies, the array cache store, and preparing Inertia, Livewire, Scout and Socialite. Removing them breaks Laravel under Octane. ## The events you can hook | Event | When it fires | Useful properties | |---|---|---| | `WorkerStarting` | once, after the worker boots the app | `$event->app` | | `RequestReceived` | before each request is handled | `app`, `sandbox`, `request` | | `RequestHandled` | after the response is built | `sandbox`, `request`, `response` | | `RequestTerminated` | after the response is sent and the kernel terminates | `app`, `sandbox`, `request`, `response` | | `TaskReceived` / `TaskTerminated` | around a Swoole task | `app`, `sandbox`, task data | | `TickReceived` / `TickTerminated` | around a Swoole tick | `app`, `sandbox` | | `OperationTerminated` (a contract) | after any request, task or tick | implemented by the three terminated events | | `WorkerErrorOccurred` | on an unhandled worker error | the exception and sandbox | | `WorkerStopping` | when the worker shuts down | `app` | Two properties matter most: - `$event->app` is the **base** application, shared by all requests on the worker; - `$event->sandbox` is the **clone** handling this request. ## Choosing where to reset 1. **`RequestReceived`** - reset before the request runs. This is the safe default: even if the previous request crashed before its terminate hooks, the next one starts clean. 2. **`OperationTerminated`** - clean up after requests, tasks and ticks alike; good for releasing memory promptly. 3. **`RequestTerminated`** - request-only cleanup that needs the response. Resetting before is the defensive choice; resetting after frees memory sooner. Some teams do both for sensitive state. ## Walking through Octane's own example The docs' leaking controller appends to `Service::$data` on every request. With a listener, the reset is a one-liner in `handle()`: set `Service::$data = []`. Better still, ask why the data is static at all: if it is a per-request buffer, a `scoped()` service or a local variable passed along does the job without any listener; if it is a genuine cross-request cache, it needs a size bound and keys that cannot mix users. ## Writing the listener - **Keep it cheap**: it runs on every request, so avoid resolving heavy services or doing I/O. - **Reset only state you own**, for example `Registry::$items = []`; leave framework state to Octane's listeners. - **Type-hint the event** (`RequestReceived $event`) so the listener documents when it runs. - **Prefer container tools for instances**: if the state lives on a container binding rather than a static, a `scoped()` binding or the `flush` array is simpler than a hand-written listener. - **Test it** with two requests on one worker, asserting the second sees no trace of the first. ## What the default config leaves off Two listeners ship commented out under `OperationTerminated`: `DisconnectFromDatabases` (close every database connection after each operation) and `CollectGarbage` (run `gc_collect_cycles()` above the `garbage` megabyte threshold). They are there to enable when your workload needs them.
- Why reset in RequestReceived rather than only in RequestTerminated?RequestReceived runs before every request, so the next request starts clean even if the previous one failed before its terminate hooks ran. Resetting after the request frees memory sooner. For state that could expose one user's data to another, resetting before the request is the safer default.
- What is the difference between $event->app and $event->sandbox in an Octane listener?`app` is the worker's base application, booted once and shared by every request. `sandbox` is the clone Octane created for this request and discards afterwards. Reset or re-point per-request state on the sandbox; touch the base app only for things that must change for all later requests.
saying these in an interview costs you the question
- You can drop Octane's spread listeners and keep only your own
- Octane clears every static property in the app automatically
- RequestTerminated is guaranteed to run, so resetting there is enough
- OperationTerminated fires only after HTTP requests, not tasks or ticks
- A growing static array is harmless because PHP frees it at response end