Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jwh's avatar
Level 2

Are singleton bindings in the Service Container persisted between jobs in the queue?

I'm running into some really strange problems with Horizon jobs, which seem to have access to models they've never loaded.

My application registers a singleton in the Service Container when it boots: CompanyService is a singleton that performs various tasks on a single contextual Company model. I specifically need CompanyService to be a singleton, as it is very resource-intensive to set up.

A typical queued job might loop through a group of models from e.g. Company::all(). Inside the loop, it calls CompanyService::setContext($company) — this sets a property on the CompanyService class to the $company model. Throughout the rest of the CompanyService class, all actions take place on the contextual model it's been assigned to work on.

I've noticed some jobs seem to be changing Company models they couldn't possibly have loaded. My hunch is that Horizon is persisting the same instance of the singleton between jobs. I scoured through the docs, and found this note:

"any static state created or modified by your application will not be automatically reset between jobs"

I'm being careful to apply the context before each action is performed within a job. But I'm wondering if there's a race-condition going on — if one job calls a long-running action on CompanyService, another simultaneous job could have changed the contextual Company to complete its own tasks — and the first job's contextual Company gets switched-out, directly underneath it?

If that's the case, how can I work around this? Is there a way to detect when application code is being run within a queue worker process?

I really hope that makes sense... I was up until 2am last night trying to figure this out and my brain feels like fudge :(

0 likes
5 replies
mabdullahsari's avatar

Application instances are not shared between workers. Each worker has its own app instance. If a singleton binding is resolved in a worker, and that same worker needs access to that same binding later on, then the old binding will be re-used. The answer to your question in the title is "yes".

Reset your application to its initial state and you should be fine. That means, resetting those static properties after the job has either failed or succeeded.

2 likes
jwh's avatar
Level 2

Each worker has its own app instance, but if a worker isn't terminated, the next job assigned to it will keep using the same instance — is my understanding correct there?

I'll look into resetting the application state... I just wish I'd anticipated this from the beginning! My code example was greatly simplified for brevity, I've got several very complex and interdependent classes that are built like this. It worked perfectly in the browser and saved so much code duplication :'(

mabdullahsari's avatar

Yes, you are correct. If the static field is null initially, you should make sure it is null again after the job has run.

This is why job idempotence is very important. Static properties are bound to cause headaches in long-running processes. Mohamed Said's book "Laravel Queues in Action" touches on this topic very nicely.

1 like
jwh's avatar
Level 2

I ended up using queue handler events to reset the application state between each job. This way I didn't have to rewrite all my context-based services. In the boot() method of a service provider, I'm doing something like this:

// Scope the context to the current job's unique identifier
Queue::before(function (JobProcessing $event) {
    CompanyService::scopeContext($event->job->uuid());
});

// Reset the application, ready for the next job to run
Queue::after(function (JobProcessed $event) {
    CompanyService::clearContext($event->job->uuid());
});

I "scope" the context to the job's UUID, because the CompanyService is invoked as a singleton. The static property holding the contextual Company was then changed to a key/value array, with the key set as the job's UUID.

In hindsight, I wouldn't have written the system to depend on static context. But this was a good workaround to save me unpicking weeks of work. :)

noar_songa's avatar
Level 1

As of Laravel 8, it is now possible to register scoped singletons where the instance is reset when the framework starts a new "lifecycle" for a request or a job, as mentioned in the Laravel Service Container Documentation.

So you can now use the scoped method in replacement of singleton in your AppServiceProvider and the state of your singletons will no longer be persisted between jobs in the queue.

1 like

Please or to participate in this conversation.