DEV Community

Cover image for Laravel's scoped(): The Singleton That Knows When to Die
Othmane Nemli
Othmane Nemli

Posted on

Laravel's scoped(): The Singleton That Knows When to Die

Laravel's service container has a familiar method:

$this->app->singleton(MyService::class);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

If registered as a singleton:

$this->app->singleton(RequestContext::class);
Enter fullscreen mode Exit fullscreen mode

Request A might do:

$context->userId = 10;
Enter fullscreen mode Exit fullscreen mode

Then Request B could receive the same instance:

$context->userId; // 10
Enter fullscreen mode Exit fullscreen mode

That state has crossed a request boundary.


Enter scoped()

Laravel provides:

$this->app->scoped(RequestContext::class);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 than singleton().

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.
Enter fullscreen mode Exit fullscreen mode

That small difference becomes extremely important once PHP stops dying after every request.

Top comments (0)