skip to content

State Leaks Between Requests

Octane boots the app once per worker, so singletons holding the request, config or container and static arrays outlive a request. Interviewers ask how you find and reset them.

on this pageshow

explore

questions

6

Under Laravel Octane, how often do service providers' register() and boot() methods run, and why is reading request() or auth()->user() inside boot() a bug?

level: juniorimportance: must knowfreq 50%

answer

  1. once per worker, not per request
  2. deferred providers loaded at boot too
  3. boot-time request built from app.url
  4. per-request work in middleware
  5. facades re-resolved each request

basics

~20 s

Under Octane, providers' register() and boot() run once per worker, at boot. The request then is a synthetic GET built from app.url and nobody is logged in, so whatever boot() reads from request() or auth() is wrong for real requests.

solid answer

~40 s

Octane boots the application once per worker: it runs the bootstrappers, including every provider's `register()` and `boot()`, and also loads deferred providers up front. Later requests are served by a clone of that booted app, so provider code never runs again until the worker restarts. Octane inserts Laravel's `SetRequestForConsole` bootstrapper before providers register, so during boot `request()` is a synthetic `GET` built from `config('app.url')`, and no user is authenticated. Code in `boot()` such as `View::share('user', auth()->user())` or storing `request()->getHost()` captures that boot-time state and serves it to every request on the worker. Registering things is fine in `boot()`; reading per-request data belongs in middleware, controllers, view composers or callbacks that run during the request.

code

php · 20 lines
php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Wrong under Octane: runs once per worker, user is null
        // View::share('currentUser', auth()->user());

        // Right: the composer callback runs on every render
        View::composer('layouts.app', function ($view) {
            $view->with('currentUser', auth()->user());
        });
    }
}

go deeper

for a junior

Remember that Octane runs register() and boot() once per worker, so boot() must not read the request or the logged-in user.

for a middle

Explain the boot versus per-request split: SetRequestForConsole's synthetic request, loadDeferredProviders, and why callbacks registered at boot are fine if they read state when they run.

for a senior

Show you audit providers for captured request, auth and locale state when moving an app to Octane, and push per-request logic into middleware or callbacks.

for a principal

Frame provider hygiene as a team rule for Octane readiness, with review checks, rather than fixing leaks one incident at a time.

## One boot per worker Under PHP-FPM, every request builds a fresh Laravel application: providers register, providers boot, the request is handled, and everything is thrown away. **Laravel Octane** splits that in two: 1. **Worker boot** - once per worker process or thread. Octane's `ApplicationFactory` requires `bootstrap/app.php`, runs the HTTP kernel's bootstrappers (loading environment and config, registering and booting providers), calls `loadDeferredProviders()`, and pre-resolves the services in `octane.warm`. 2. **Per request** - for every request the worker clones that booted application into a **sandbox**, runs Octane's reset listeners on it, sends the request through the HTTP kernel, and discards the sandbox afterwards. So `register()` and `boot()` of every provider - including ones marked as deferred, which Octane loads eagerly - run **once per worker**, not once per request. A worker that serves 500 requests before it is recycled ran your `boot()` once. (What belongs in `register()` versus `boot()` in general is a separate container topic.) ## What the request looks like during boot At boot time no real HTTP request exists yet. Octane handles this by inserting Laravel's `SetRequestForConsole` bootstrapper just before providers are registered. It creates a `GET` request from `config('app.url')` (default `http://localhost`) and binds it as `request`. Consequences inside a provider's `boot()`: - `request()->getHost()`, headers, query string, cookies: those of the synthetic request, not of any user; - `auth()->user()`: `null`, because no session or token was presented; - `app()->getLocale()`: the configured default, not a locale negotiated per request. Whatever `boot()` reads, it reads **once**, and whatever it stores lives as long as the worker. ## Typical bugs | Code in `boot()` | Under PHP-FPM | Under Octane | |---|---|---| | `View::share('currentUser', auth()->user())` | the current user, every request | `null` for every request on that worker | | `$tenant = Tenant::forHost(request()->getHost())` stored somewhere | right tenant per request | the tenant for `app.url`, for everyone | | `URL::forceRootUrl(request()->root())` | per-request root | the boot-time root, forever | | reading a feature flag from a request header | per request | fixed at boot | ## Where per-request code belongs - **Middleware** - runs inside each request with the real request and the authenticated user. - **Controllers, form requests, jobs** - receive the data they need as arguments. - **View composers** registered in `boot()` - the *registration* happens once, but the composer callback runs each time the view renders, so reading `auth()->user()` **inside the callback** is correct. - **Closures and callbacks** in general: registering a callback at boot is fine as long as the callback fetches `request()`, `auth()` or `config()` when it runs, not when it is registered. ## What Octane already takes care of Octane resets most first-party state for you on each request: it gives the new request to the application, clones the configuration repository, forgets resolved authentication guards, and points facades at the new sandbox (`Facade::clearResolvedInstances()` runs each time the current application is set). That is why using `request()`, `auth()` and facades **during a request** is safe. The problem is only code that captures those values **at boot** and keeps them. ## Beyond the request: other boot-time captures The same reasoning applies to anything that varies per request: - **Locale** - calling `App::setLocale()` from a header inside `boot()` fixes the locale for the worker; Octane's `FlushLocaleState` listener restores the configured locale before each request, so per-request locale belongs in middleware. - **Time** - storing `now()` in a property at boot gives every request the worker's start time. - **Tenant or user-specific configuration** - computing it in `boot()` bakes one tenant's values into the worker. Things that are genuinely global - macros, gate definitions, event listener registrations, route model binding rules, `Model::preventLazyLoading()` - are exactly what `boot()` is for, under Octane as under PHP-FPM. ## How to spot it - The bug appears only under Octane, not under `php artisan serve` or PHP-FPM. - The wrong value is constant for a worker and changes only when workers restart. - Local development with `--watch` may hide it partially, because every file change reboots the workers. A quick review rule: in any provider, look for `request(`, `auth(`, `Auth::`, `session(` or `Request` outside a closure. Each is a candidate for moving into middleware or into a callback that runs per request.

  • Does a deferred service provider still save work under Octane?
    Not per request. Octane's application factory calls `loadDeferredProviders()` during worker boot, so deferred providers are registered up front. The cost moves to worker start-up, once per worker, which is usually a good trade in a long-lived process.
  • Is it safe to call request() inside a controller under Octane?
    Yes. Before each request Octane binds the new request into the application and the sandbox, so `request()`, `Request` type-hints on controller methods and the `Request` facade all see the current request. The danger is only code that captured the request earlier and kept it.

saying these in an interview costs you the question

  • Service providers boot on every request under Octane, like PHP-FPM
  • During boot, request() already holds the first real user's request
  • Deferred providers stay lazy per request under Octane
  • Facades keep the first request's instances, so they are unsafe under Octane
  • A view composer registered in boot() reads auth()->user() only once
open as a page

After moving a Laravel banking API to Octane, some responses show the previous customer's accounts; which Octane-specific state is carrying them over, and how do you fix it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Octane clones one booted app per request, but instances resolved at boot or warmed, and static properties, are shared by every request on the worker. A singleton or static cache that memoizes the current customer serves them to the next request.

open as a page

In Laravel Octane, what do the --max-requests option and the garbage setting do about memory growth, and why are neither of them a fix for a leak?

level: middleimportance: should knowfreq 35%

basics

~10 s

--max-requests (default 500) gracefully replaces a worker after that many requests. garbage (50 MB) is read only by the CollectGarbage listener, which ships commented out. Both contain memory growth; neither removes a leak.

open as a page

In Laravel Octane, how do you reset your own static state before each request through the listeners config, and which events can you hook?

level: middleimportance: should knowfreq 25%

basics

~20 s

Add 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.

open as a page

Under Laravel Octane, why can a singleton that received the container, the request or the config repository in its constructor see stale values, and what are the fixes?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A singleton built at boot keeps the objects it was given, while Octane gives each request a new request, a cloned config and a cloned container. Inject resolver closures, use app(), request() and config(), or pass values to methods.

open as a page

In Laravel Octane's config/octane.php, what do the warm and flush arrays do, and when would you add your own binding to either?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

warm lists bindings Octane resolves once at worker boot, shared by later requests; flush lists bindings Octane forgets after every request, so they are rebuilt next time. Warm only stateless services; flush ones holding per-request state.

open as a page