Skip to content

Laravel error

Laravel: Job has been attempted too many times

The error

App\Jobs\SyncOrder has been attempted too many times or run too long. The job may have previously timed out.

A MaxAttemptsExceededException is the queue giving up, and it is a symptom rather than a cause. What it tells you depends on whether the job was failing or timing out.

Illuminate\Queue\MaxAttemptsExceededException
App\Jobs\SyncOrder has been attempted too many times or run too long.
The job may have previously timed out.

The queue has stopped retrying. That is all this exception says - it is the record of giving up, not the record of what went wrong.

Finding the cause means deciding which of two stories happened, and the message deliberately refuses to guess.

Was it failing, or was it timing out?

Failing leaves evidence. An exception at the time of the first attempt, in the log, in your error reporting, with a stack trace. The job tried, threw, was retried, threw again.

Timing out leaves nothing. The worker is killed partway through; it has no opportunity to record anything; the message goes back on the queue because it was never acknowledged. The next worker sees only that the attempt count has gone up. Silence followed by this exception is the signature.

The practical test: look for an entry from the time of the first attempt, not the last. If there is one, fix that. If there is not, the job is too slow for its timeout.

The configuration that causes it without any failure at all

retry_after on the queue connection has to be longer than the longest a job can run:

// config/queue.php
'redis' => [
    'retry_after' => 90,     // must exceed the job's timeout
],
class SyncOrder implements ShouldQueue
{
    public $timeout = 120;   // longer than retry_after - a problem
    public $tries = 3;
}

With these numbers the connection releases the job back to the queue at ninety seconds while the first worker is still working on it. A second worker starts it. Both run; the attempt count climbs; eventually it exceeds tries and you get this exception for a job that never failed once.

Worse, the work happened twice. If it was not idempotent, the exception is the least of what went wrong.

Keep retry_after comfortably above every job's timeout, and set the timeout per job rather than relying on one global number.

Then decide what "too many" should mean

tries is not a dial to turn up until the error stops.

For a transient failure - a rate limit, a network blip, a provider's bad minute - retries are the right tool, and a backoff makes them effective rather than just more frequent:

public $tries = 5;
public array $backoff = [10, 30, 120, 600];

For a deterministic failure - a malformed record, a missing relation, a bug - retrying is a way of failing five times instead of once. Those jobs want tries = 1 and a human.

The distinction is worth encoding. A job that can tell the difference should stop itself:

public function handle(): void
{
    if (! $this->order->exists) {
        $this->fail(new OrderDeleted($this->orderId));   // no further attempts
    }
}

The thing to fix underneath

If the job times out, the usual causes are a loop over records that grew, an external call with no timeout of its own, or work that should have been several jobs. Splitting it is almost always better than extending the timeout: a job that takes two minutes today takes six when the data trebles, and the deploy that restarts workers will interrupt it either way.

And whatever the cause, the retry itself has consequences. A queue is at-least-once by design, so a job that half-finished before timing out will do that half again. Making jobs safe to repeat is what turns this exception into an operational annoyance rather than an incident.

Queue behaviour under load is the sort of thing that looks fine until it does not, which is what the work we do on job pipelines is mostly about.

Related questions

What does "or run too long" mean - which was it?
The message cannot tell, which is why it says both. A worker killed on timeout does not get to record why, so the next attempt only sees a job that has been tried before. If the job threw, there is an earlier entry in the log; if it timed out, there is silence and then this.
Should we just raise tries?
Only if the failure is genuinely transient and the job is safe to repeat. Raising it on a job that fails deterministically buys the same failure a few more times, and raising it on a job that is not idempotent multiplies whatever it already did.
Why is the job in failed_jobs with no exception we recognise?
Because the exception recorded is this one - the giving-up, not the cause. The real failure, if there was one, is in the log at the time of the first attempt. If there is nothing there, it timed out.
Does retry_after matter here?
It matters more than most settings. If the queue connection releases a job back before the worker finishes it, a second worker picks it up while the first is still running, and the attempt count climbs without anything failing. retry_after must be larger than your longest job timeout.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page