Skip to main content
To make a package usable not only in regular HTTP requests but also in Octane and queue workers, you need to decide how long a service lives and what it is allowed to hold. If you store per-request data in a singleton, a different operation may end up reading stale data. This page examines the container, queue worker, and Octane implementations as of Laravel Framework v13.36.0, and connects the basic registration methods of the service container to the long-term maintenance of a package. Simply choosing scoped() does not reset every reference between objects or every piece of static state.

Choose a registration method based on how long the service lives

This table compares regular resolution. Check special paths such as resolution with explicit parameters or contextual binding separately. Even with bind(), if you store the retrieved service in a property of another singleton, that reference remains. Registration methods are rules for how the container creates and reuses objects; they do not force objects held by consumers to be recreated.

scoped combines a shared registration with a record of what to remove

The Container::scoped() method we examined does two things:
  1. It records the registration key in scopedInstances.
  2. It calls singleton() and registers the service as a shared binding.
The instance itself is stored in the same instances array as singletons. forgetScopedInstances() calls unset() on the stored instances for the recorded keys. Because the binding’s factory remains, the next regular resolution can create a new object. This process does not reset the properties of the old object, nor does it rewrite references held by other objects. An old object that is still referenced can continue to be used.

The runtime creates the boundary

The container itself does not watch for the start of HTTP requests or jobs.
  • Queue workers: QueueServiceProvider passes a callback that resets the scope to the Worker. Worker::daemon() calls it in the loop before fetching the next job, and the callback performs tasks such as forgetScopedInstances() and clearing resolved Facade instances.
  • Octane: The official default configuration registers FlushTemporaryContainerInstances for OperationTerminated. This listener calls forgetScopedInstances().
  • Custom long-running commands or loops: Simply calling make() repeatedly does not create a boundary. The runtime must design the lifecycle.
Do not call forgetScopedInstances() on your own from regular application code or from a package’s boot(). It affects every scoped service in the container. Also, not every job path, including synchronous execution, necessarily goes through the same reset path as a long-running Worker.

Make state shared within an operation scoped

Consider a delivery package that wants to share a destination identifier within a single request or job. Rather than holding the HTTP Request directly, it stores only the value it needs.
In the package’s service provider, register an empty context without reading the consumer’s state at creation time.
In the consuming application’s controller or job handle(), set an identifier that has already passed input validation and authorization checks. Other services that resolve this type within the same lifecycle can refer to the same context.
This context is not an authorization mechanism. Being able to set an identifier is not the same as being permitted to access that destination. Also, scoped() does not persist anything to a database or an external cache. Pass values needed by the next job explicitly as job data.
In Octane, a service provider’s register() and boot() run when the worker first starts. Do not design your package to store the current user or destination in the context from boot(). Set per-operation values after the operation has started.

References from a singleton to a scoped service are not updated automatically

With the following registration, making only the context scoped does not work as intended.
If DeliveryService stores the context in a property in its constructor, it keeps holding the first context it was created with. Even if the container removes its stored context, the property of the already created DeliveryService does not change. Choose a remedy that fits the service’s responsibilities.
  1. Pass the values needed for each operation as method arguments. If there is no need to share state, this is the simplest option.
  2. Make the holder scoped as well. Recreate both DeliveryService and the context per operation.
  3. Pass a resolver that resolves at the time of use. If the service must remain a singleton, retrieve the needed service from the current container each time.
In the third example, rather than capturing the old context in a closure, resolve it at call time.
The DeliveryService in this example must be changed to accept a Closure in its constructor and expose an API that calls it on every operation. Calling the resolver only once in the constructor and storing the result does not solve the problem. This is a design example that applies the container resolver from the official Octane documentation to a package-specific context.

Check for state that scoped does not clear

scoped() does not automatically clear the following state:
  • Values added to the package’s own static properties
  • References to the context stored in another singleton or a global variable
  • A “current user” or “current destination” held inside a singleton
  • References held by custom Facades or cache layers
Distinguish between resets performed by the standard runtime and resets of the package’s own state. Reducing the places that hold per-operation state is simpler than maintaining custom clearing logic for every runtime.

Write tests that cross a boundary in the same container

If your HTTP tests only create a new application each time, you may miss problems that arise when a long-running process keeps using the same container. Start by verifying, with the container alone, that state is shared within an operation and recreated after the boundary. The following tests use PHPUnit. Like the official Container tests, the first test uses forgetScopedInstances() to check the effect of the boundary without starting an entire Worker.
The second test documents the behavior where references remain. In a real package, also add tests that verify the remedy you adopted uses the new context or value. Container-only tests do not verify Octane’s sandbox or the actual execution path of queue workers. You also need integration tests that run consecutive operations in each runtime you claim to support. For deployments that change registration methods or service implementations, reload long-running processes so that the new code takes effect.

Package service resolution hooks

Review afterResolving, retrieval of shared instances, and how configuration differs on re-resolution.

Laravel Octane

Review caveats about holding the Request or container and operating long-running processes.

Primary sources consulted

Last modified on October 14, 2026