laravelmastery
SnippetsArticlesSeriesNews

Queues in production · Lesson 2 of 3

Queues in production · Lesson 2

Batches and then()

When it goes wrongQueues

Once order 1042 is paid, its invoice needs a PDF and an email. They can run as two jobs, and you only want to mark the invoice sent when both have succeeded. A batch is that group, and then() is the callback for “all of them worked”.

Video

01 Run the follow-up when the batch succeeds

Each job in the batch uses the Batchable trait. then() runs when every job has succeeded. A failure cancels the jobs still waiting, catch() runs, and then() does not. finally() runs either way.

PHP
Bus::batch([
    new GenerateInvoicePdf($invoice->id),
    new EmailInvoice($invoice->id),
])
    ->name('invoice-'.$invoice->id)
    ->then(function (Batch $batch) use ($invoice): void {
        $invoice->forceFill(['sent_at' => now()])->save();
    })
    ->catch(function (Batch $batch, Throwable $exception): void {
        report($exception);
    })
    ->dispatch();

allowFailures() lets the remaining jobs continue after one fails. then() still waits for a batch with no failures. Do not mark the invoice sent in finally(): that callback also runs when the PDF failed and the email never went out.

A test that only cares the batch was dispatched uses Bus::fake() and Bus::assertBatched(). A single job, dispatched on its own, is the case the testing series fakes with Queue::fake().

02 Stop a job when the batch has already been cancelled

A failure cancels the jobs that have not started. A job that has started should notice. The Batchable trait gives handle() the batch, and cancelled() is the check. Returning leaves that job successful. It does not count as another failure.

GenerateInvoicePdf.phpPHP
class GenerateInvoicePdf implements ShouldQueue
{
    use Batchable, Queueable;

    public function __construct(public int $invoiceId)
    {
    }

    public function handle(): void
    {
        if ($this->batch()?->cancelled()) {
            return;
        }

        Invoice::query()->findOrFail($this->invoiceId)->writePdf();
    }
}

then() still does not run after a cancellation. The PDF method is skipped, the email job that was waiting is not run, and sent_at stays null. finally() can record that the batch ended. It must not be the place that marks the invoice sent.

Name the batch with the invoice id, as the dispatch already does. A failed batch is then findable in Horizon without opening every payload. The name is a label. It is not the lock, and it does not replace the cancelled() check inside the job. Two batches can share a name. The id on the job is what handle() loads.

Recap

You can mark an invoice sent only when the PDF job and the email job have both succeeded.

  • then() runs when the batch has no failures. finally() also runs when it failed.
  • allowFailures() does not make then() ignore a failure.
  • A job that has started should return when the batch is already cancelled.

Check yourself · 3 questions

Question 1 of 3The PDF job throws. Where do you set sent_at?

The invoice is sent only when both jobs succeed. Next, make the payment itself safe to run twice.

← PreviousUnique jobs and WithoutOverlapping

On this page

  • Run the follow-up when the batch succeeds
  • Stop a job when the batch has already been cancelled
  • 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.