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?
answer
- once per worker, not per request
- deferred providers loaded at boot too
- boot-time request built from app.url
- per-request work in middleware
- facades re-resolved each request
basics
~20 sUnder 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 sOctane 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
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
Remember that Octane runs register() and boot() once per worker, so boot() must not read the request or the logged-in user.
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.
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.
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