Retries and Failures

Attempts, Timeouts, Backoff, and Failed Jobs

A job gets one attempt by default. When handle() throws, the worker retries it or, after the last attempt, calls failed() and moves it to failed_jobs. Attributes override the worker's --tries, --backoff and --timeout flags:

app/Jobs/NotifyWarehouse.php (excerpt)PHP
#[Tries(3)]
#[Backoff(2, 5)]
#[Timeout(20)]
class NotifyWarehouse implements ShouldQueue
{
    public function handle(): void
    {
        Log::info("warehouse: order {$this->order->id}, attempt {$this->attempts()}");
        Http::timeout(5)->post(config('services.warehouse.url'), [
            'order' => $this->order->id,
        ])->throw();
    }
    public function failed(Throwable $e): void
    {
        Log::error("warehouse gave up on order {$this->order->id}: {$e->getMessage()}");
    }
}

#[Backoff(2, 5)] waits 2 seconds, then 5, so the attempts fell at 08:58:00, :03 and :09. php artisan queue:failed then listed the job by UUID as database@default App\Jobs\NotifyWarehouse, and its row read:

The failed_jobs rowShell
mysql -E shop -e "SELECT uuid, JSON_EXTRACT(payload, '$.maxTries') AS max_tries,
  SUBSTRING_INDEX(exception, ':', 2) AS exception, failed_at FROM failed_jobs"
Output
*************************** 1. row ***************************
     uuid: ce8a3f0c-3eb4-4012-b31d-6387e8c43ee5
max_tries: 3
exception: Illuminate\Http\Client\RequestException: HTTP request returned status code 503
failed_at: 2026-09-23 08:58:09

With the warehouse back, php artisan queue:retry all pushed the job again and it succeeded on a fresh attempt 1; queue:forget and queue:prune-failed delete rows. #[MaxExceptions(2)] caps exceptions separately from releases, retryUntil() sets a deadline, and #[FailOnTimeout] fails a timed-out job instead of retrying. Timeouts need pcntl: the worker kills itself and is restarted. Keep every timeout a few seconds below retry_after, or a slow job reaches a second worker while still running, and the customer gets two emails.