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

nicktenc's avatar

Queueable listeners randomly start to fail (and don't end up in the failed_jobs table)

Our queued listeners (triggered by an event) and also a few regularly dispatched jobs randomly start failing, when it starts to fail, we have a lot of these failed queued listeners. The (unhandled) exception that we are getting:

Unhandled exception within Laravel Queue job: 'Illuminate\Database\Eloquent\ModelNotFoundException'

Notice the message: "Unhandled exception within Laravel Queue job". I haven't seen internet posts talking about that message. When they talk about the ModelNotFoundException they talk about the message "No query results for model [App\SomeModel]'". Also notice that the exception is 'unhandled', which could explain why the failed job doesn't reach the failed_jobs table. (we keep failed_jobs in MariaDB)

In the worker.log we see that the jobs failed. But they also don't appear in the failed_jobs table. Which is odd, since we didn't set the deleteWhenMissingModels flag. If I read the documentation, the default behaviour should be that jobs with missing models should be in the failed jobs table. But although we are still researching, it looks like they are not reaching the failed jobs table or are automatically being purged/deleted. But like I said the unhandled exception could be a cause of that. In what kind of circumstance would Laravel not be able to catch/handle the exception thrown by or within a job?

But the main thing is; why are queued events failing? Normally they work fine, but when this issue is happening, 1% of the listeners start to fail. It's not 1 particular listener or 1 particular event. We have set the after_commit flag (https://laravel.com/docs/10.x/queues#jobs-and-database-transactions) which should guarantee that a listener is executed when there aren't any open transactions.

Our config looks like this:

    'redis' => [
        'driver'     => 'redis',
        'connection' => 'queue',
        'queue'      => 'default',
        'retry_after' => 9000,
        'after_commit' => true
    ],

We do have multiple queue's. Not sure if that has to be specified, but then it's still strange that the jobs do land in Redis, but don't honor the after_commit flag. We don't seem to see problems for normal jobs, that are dispatched from the code. Only the event based jobs (the listeners) have this problem.

Some additional context; a couple of weeks ago we faced problems where job queries (INSERT INTO jobs, etc) came to an halt, apparently waiting for a transaction to be commit. At that time we could find the source of the problem, and decided to move from a MariaDB queue to a Redis queue. Pretty quickly after that we started to see the problems mentioned in this ticket.

Last 2 days we didn't face any of these issues, where 3 days ago we faced hundreds of these incidents.

Would appreciate any help of this topic.

0 likes
10 replies
Snapey's avatar

In what kind of circumstance would Laravel not be able to catch/handle the exception thrown by or within a job?

If your job tries to find the related model for itself.

For instance, if you pass a model into the constructor of the job, when it is queued then only the ID of the model is queued. When the job is retrieved to be executed, the model referenced is hydrated. It is here that the missing models are handled.

However, if you pass in a model ID (just as plain int) and then do in your handle method, Model::find($this->model_id) then the query will fail and result in an unhandled exception if the model no longer exists.

nicktenc's avatar

@Snapey thanks for your help.

We noticed that some of the failed jobs, are not all listeners. So also some 'normal jobs' dispatched from the code have the same problem. An example of such a job:

namespace App\Jobs;

use App\Mail\NewDeviceLoginEmail; use App\User; use App\UserLogin; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Mail;

class NewDeviceLogin implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

protected UserLogin $userLogin;
protected User $user;

public function __construct(UserLogin $userLogin, User $user)
{
    $this->userLogin = $userLogin;
    $this->user = $user;
}

public function handle(): void
{
    if (!empty($this->user->email)) {
        $this->userLogin->getDeviceLocation();
        Mail::send(new NewDeviceLoginEmail($this->userLogin, $this->user));
    }
}

}

A missing model should be handled by Laravel, but that didn't happen in this case. This job is being dispatched here:

    $userLogin->save();

    if ($isNewDeviceLogin) {
        dispatch(new NewDeviceLogin($userLogin, $user));
    }

The user model is not modified or created in that function. Only the userLogin. You can see that it's being saved before we dispatch the NewDeviceLogin job.

nicktenc's avatar

@Snapey do you have an idea where this particular message is coming from?

Unhandled exception within Laravel Queue job

Snapey's avatar

@nicktenc what makes you think its missing model? Which of these UserLogin or User might be missing?

nicktenc's avatar

@Snapey in my opinion they can't be missing. Not as in 'the record does not exists'. The user was created months before that event. And the user login model was also created and saved. We didn't wrap it around a transaction. But still the job throws a:

Unhandled exception within Laravel Queue job: 'Illuminate\Database\Eloquent\ModelNotFoundException'

So the exception is a ModelNotFoundException, but from what I can see there are more reasons that trigger this exception?

Snapey's avatar

@nicktenc You could have unhandled exception if, for instance, your Mail call failed. Try wrapping the whole thing in try-catch and seeing what error you get.

public function handle(): void
{
	try {
        if (!empty($this->user->email)) {
            $this->userLogin->getDeviceLocation();
            Mail::send(new NewDeviceLoginEmail($this->userLogin, $this->user));
        }
    } catch (\Throwable $th) {
            Log::error('Error in  queued job:' . $th->getMessage());
    }
}
nicktenc's avatar

@Snapey thanks, but normally Laravel handles failed jobs, and puts them in a failed_jobs table. Wouldn't that prevent that from happening when I manually start catching these exceptions on production?

nicktenc's avatar

@Snapey for now we have added the Queue::failing, to do some additional logging on there. But if the exception is unhandled, that won't probably work. Will see if we can wrap some unimportant (but randomly failing jobs) around a try / catch block. Thanks!

nicktenc's avatar

I also don't expect that it actually reaches the handle method... the jobs ran for 2-4 ms, don't think thats enough for setting up a DB connection and fetching a row?

Please or to participate in this conversation.