laravelmastery
SnippetsArticlesSeriesNews

Queues in production · Lesson 2 of 3

Queues in production · Lesson 2

Failed jobs you can replay

When it goes wrongQueues

The backoff has run out of tries. After the last try, Laravel stores the job in failed_jobs. You can inspect it and run it again once the cause is fixed. A failed job is a record, not a dead end.

Video

01 Replay one, or all of them

failed() runs once, when the job has given up. Keep it small: report the exception, and leave the order row alone if handle() might have written it already.

Example.phpPHP
class Example
{
    public function failed(Throwable $exception): void
    {
        report($exception);
    }
}
PHP
php artisan queue:failed
php artisan queue:retry 8f3c
php artisan queue:retry all

Read the exception before you retry. Until handle() is safe to run twice — lesson 8 — don't replay a job that charges a card. Retry is safe when handle() can run twice and still leave the order paid once.

02 Keep the order id next to the exception

report() stores the exception. It does not, by itself, say which order it belonged to. failed() still has the constructor arguments, so the log line can name them. Leave the order row alone when handle() might have written it already. A second write here can mark a paid order as failed.

Example.phpPHP
class Example
{
    public function failed(Throwable $exception): void
    {
        logger()->error('MarkOrderPaid gave up', [
            'order_id' => $this->orderId,
            'attempt' => $this->attempt,
            'message' => $exception->getMessage(),
        ]);

        report($exception);
    }
}

queue:retry copies that same payload back onto the queue. It does not build a new attempt key. If the logged attempt is already on the order, the idempotent handle() returns and the retry is a no-op. That is the outcome you want after a worker died at the end of a successful charge.

Recap

You can read a failed job and retry the same payload once handle() is safe to run again.

  • failed() runs once, when the tries are spent.
  • queue:retry copies the payload. It does not mint a new attempt UUID.
  • Do not write the order again from failed() if handle() might have written it.

Check yourself · 3 questions

Question 1 of 3queue:retry runs the same payment UUID that is already on the order. What should happen?

A failed job keeps the original payload. Next, stop a second click dispatching a twin.

← PreviousRetries, backoff and timeouts

On this page

  • Replay one, or all of them
  • Keep the order id next to the exception
  • Recap
  • Check yourself

Queues in production

  • 1 Overview

Hand the work off

  • 2 Keep the request short
  • 3 The job class
  • 4 Dispatch from actions, not controllers

When it goes wrong

  • 5 Retries, backoff and timeouts
  • 6 Failed jobs you can replay
  • 7 Unique jobs and WithoutOverlapping
  • 8 Batches and then()

Run it for real

  • 9 Make the job safe to run twice
  • 10 Horizon in production

Queues in production

Lessons

  1. 01Overview

Hand the work off

  1. 02Keep the request short
  2. 03The job class
  3. 04Dispatch from actions, not controllers

When it goes wrong

  1. 05Retries, backoff and timeouts
  2. 06Failed jobs you can replay
  3. 07Unique jobs and WithoutOverlapping
  4. 08Batches and then()

Run it for real

  1. 09Make the job safe to run twice
  2. 10Horizon in production

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.