Laravel's service container has a familiar method:
$this->app->singleton(MyService::class);
It means Laravel creates the service once and reuses that instance. But there is an interesting question:
What happens when your PHP application doesn't restart between requests?
That's where scoped() comes in.
The problem with long-running workers
With the traditional PHP request lifecycle, things look roughly like this:
Request A -> Laravel boots -> Request ends -> PHP ends
Request B -> Laravel boots -> Request ends -> PHP ends
A singleton therefore appears to live only for one request.
But long-running environments such as Laravel Octane and queue workers work differently:
Long-running worker
|
|-- Request A
|-- Request B
|-- Request C
|-- Request D
The Laravel application stays alive, that means a singleton can also stay alive.
Imagine a service that keeps some request-specific state:
class RequestContext
{
public ?int $userId = null;
}
If registered as a singleton:
$this->app->singleton(RequestContext::class);
Request A might do:
$context->userId = 10;
Then Request B could receive the same instance:
$context->userId; // 10
That state has crossed a request boundary.
Enter scoped()
Laravel provides:
$this->app->scoped(RequestContext::class);
A scoped binding behaves like a singleton inside the current lifecycle.
So during one request:
$a = app(RequestContext::class);
$b = app(RequestContext::class);
$a === $b; // true
But when the lifecycle changes, Laravel clears the scoped instance and creates a new one. Conceptually:
Request A
|
|-- RequestContext #1
Request B
|
|-- RequestContext #2
Request C
|
|-- RequestContext #3
Laravel documents scoped bindings specifically for this kind of lifecycle: they are resolved once per request or job lifecycle and flushed when a new lifecycle begins.
Singleton vs scoped
The easiest way to remember the difference is the lifetime:
| Binding | Lifetime |
|---|---|
bind() |
New instance when resolved |
singleton() |
Same instance for the application/container |
scoped() |
Same instance for the current request/job lifecycle |
The important distinction is between the last two.
singleton() answers:
“Should this application use the same instance?”
scoped() answers:
“Should this execution use the same instance?”
Laravel actually resets scoped instances
This isn't just documentation terminology, Laravel's queue worker explicitly calls forgetScopedInstances:
$app->forgetScopedInstances();
when resetting the worker between jobs.
The container also exposes forgetScopedInstances() as a dedicated operation, showing that scoped instances have their own lifecycle management.
So the next job doesn't inherit the scoped objects created by the previous one.
When should you use scoped()?
Use it when an object should be shared during one execution, but must not leak into the next one.
Good examples can include:
- request-specific state
- tenant-specific state
- per-job state
- objects that accumulate information while processing one operation
A useful rule is:
If the object contains state that belongs to one request or job,
scoped()is often a better fit thansingleton().
And this becomes particularly important when your application runs in a long-lived process.
At the end
scoped() isn't really about creating another kind of singleton, it's about lifetime management.
Laravel is telling the container:
Reuse this object...
|
v
but only while this execution is alive.
That small difference becomes extremely important once PHP stops dying after every request.
Top comments (0)