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.
