skip to content

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%

answer

  1. captured at boot, kept forever
  2. request: boot-time synthetic one
  3. config cloned per request
  4. resolver closure instead of object
  5. app(), request(), config() helpers

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.

solid answer

~40 s

Octane handles every request in a clone of the booted application, binds a fresh `request` into it, and gives it a cloned `config` repository through its `CreateConfigurationSandbox` listener. A singleton constructed at boot with `$app`, `$app['request']` or `$app->make('config')` holds the **base** container, the **boot-time** request and the **base** config: it misses bindings added later, reads the wrong headers and input, and never sees per-request `config()->set()` changes. Octane's docs give three fixes: don't bind it as a singleton, inject a resolver closure such as `fn () => Container::getInstance()` or `fn () => $app['request']`, or best, pass the specific values the object needs to its methods at runtime. The `app()`, `request()` and `config()` helpers always return the current instances, and type-hinting `Request` on controller methods is fine.

code

php · 20 lines
php
<?php

use App\Services\StatementService;
use Illuminate\Container\Container;
use Illuminate\Contracts\Foundation\Application;

// In a service provider's register() method

// Stale under Octane when resolved at boot
$this->app->singleton(StatementService::class, function (Application $app) {
    return new StatementService($app['request'], $app->make('config'));
});

// Safe: resolvers fetch the current instances when called
$this->app->singleton(StatementService::class, function (Application $app) {
    return new StatementService(
        fn () => $app['request'],
        fn () => Container::getInstance()->make('config'),
    );
});

go deeper

for a junior

Remember not to store the container, the request or config in a singleton's constructor under Octane; use request(), config() and app() when needed.

for a middle

Explain what Octane gives each request - a sandbox container, a new request, a cloned config - and why a boot-built singleton misses all three.

for a senior

Show you review constructors and factories for captured references, prefer method arguments, and know the resolver-closure and rebinding options.

for a principal

Set conventions that keep services stateless and request data explicit, so Octane safety does not depend on resolution order.

## What each request actually gets Under **Laravel Octane**, each worker boots one base application and, for each request: - clones it into a **sandbox** and makes the sandbox the current container (`Container::setInstance()`, `app()`, and the facade root all point at it); - binds the new HTTP request as `request` on both the base app and the sandbox (`GiveNewRequestInstanceToApplication`); - replaces `config` in the sandbox with a **clone** of the configuration repository (`CreateConfigurationSandbox`), so a `config()->set()` made during one request does not leak into the next. Anything that fetches these objects **when it needs them** sees the current ones. Anything that captured them **once** keeps the old ones. ## The three stale-reference cases Octane's docs describe each with a singleton whose factory runs during boot: | Injected at boot | What it keeps | Symptom | |---|---|---| | `$app` (the container) | the base container | misses bindings registered later in the boot cycle or by a later request | | `$app['request']` | the synthetic boot-time request | wrong headers, input, query string and user data on every request | | `$app->make('config')` | the base configuration repository | never sees values set per request, for example per tenant | The config case is subtle. Octane's per-request clone means that changing configuration during a request is **isolated**: the next request starts from the base values again. A service holding the base repository therefore reads values that are not "stale from last request" but "never updated for this request". A tenant-aware app that sets `database.connections.tenant.database` in middleware and a mailer-like singleton that captured config at boot will disagree about which tenant is active. ## The fixes Octane's docs give 1. **Do not register it as a singleton.** A plain `bind()` builds a new object each time it is resolved, with the current request or config. 2. **Inject a resolver instead of the object.** Give the constructor a closure and call it when needed: - `fn () => Container::getInstance()` for the container; - `fn () => $app['request']` for the request; - `fn () => Container::getInstance()->make('config')` for configuration. 3. **Pass the values the method needs.** The most recommended option for request data: `$service->method($request->input('name'))`. The object stays stateless and trivially safe. And the helpers are safe by design: the global `app()`, `request()` and `config()` helpers, and `Container::getInstance()`, always return the current instances. Type-hinting `Illuminate\Http\Request` on a controller method or route closure is also fine, because the container resolves it for the current request. ## Why the timing matters These leaks depend on **when** the singleton is built. If its factory runs during a request, Octane stores the instance in that request's sandbox and discards it afterwards, so it happens to work. If anything resolves it during boot - a provider's `boot()`, a macro, an event listener registration, or the `octane.warm` list - it becomes shared across all requests on the worker. Code that "works" only because of resolution order is one refactor away from breaking, which is why the docs recommend resolvers or method arguments regardless. ## A related technique: rebinding callbacks Laravel's own services sometimes subscribe to changes with `$app->rebinding('request', ...)`. Because Octane binds each new request with `instance()`, which fires rebinding callbacks when the key is already bound, a long-lived service can receive the new request that way. Laravel's `AuthServiceProvider` uses this pattern. It is more machinery than a resolver closure and easy to get wrong, so reserve it for framework-level code. ## Putting it together: a tenant example A multi-tenant banking API sets a per-tenant value in middleware, for example `config(['statements.path' => $tenant->statementsPath()])`. A `StatementService` singleton resolved at boot builds file paths from `$this->config->get('statements.path')`, using the repository it captured. Under Octane that repository is the base one, which the middleware never touched, so every tenant's statements go to the default path, while code in the same request that calls `config('statements.path')` sees the tenant's value. The two parts of one request disagree. Rewriting the service to call `config()` when it builds each path, or to take the tenant as a method argument, removes the mismatch. ## Checklist for a code review - Constructors of singletons that type-hint `Application`, `Container`, `Request` or `Repository` (config); - factories that pass `$app['request']` or `$app->make('config')`; - services resolved in providers' `boot()` or listed in `octane.warm`; - any class that keeps `$this->request` or `$this->config` as a property.

  • Under Octane, does config()->set() inside one request leak into the next request?
    No. Octane's `CreateConfigurationSandbox` listener gives each request a clone of the configuration repository, so a `set()` affects only the current request. The flip side is that a singleton holding the base repository never sees those per-request values.
  • Is constructor injection of Request into a controller a problem under Octane?
    Octane's docs say type-hinting `Request` on controller methods and route closures is acceptable. The risk is a long-lived object keeping the request as a property. Prefer method injection or `request()` in anything that can outlive the request.

saying these in an interview costs you the question

  • Octane updates the request inside every object that captured it
  • config()->set() in one request carries over to the next under Octane
  • The request() helper is unsafe under Octane and must be avoided
  • A singleton holding $app always sees bindings added later
  • Method-injecting Request into controllers leaks under Octane