Job Batching

A batch runs jobs in parallel and reports on them as a group. Members use the Batchable trait (make:job --batched) and begin with if ($this->batch()?->cancelled()) { return; }. The invoice job renders a Blade view to storage/app/private/invoices/ and throws for a cancelled order:

Month-end invoicing as a batch (routes/console.php)PHP
Artisan::command('shop:invoices', function () {
    $jobs = Order::orderBy('id')->get()->map(fn (Order $o) => new GenerateInvoice($o));
    $batch = Bus::batch($jobs)
        ->name('September invoices')
        ->allowFailures()
        ->then(fn (Batch $b) => Log::info("{$b->name}: every invoice rendered"))
        ->catch(fn (Batch $b, Throwable $e) => Log::warning("first: {$e->getMessage()}"))
        ->finally(fn (Batch $b) => Log::info("finally: {$b->processedJobs()} processed"))
        ->dispatch();
    $this->info("batch {$batch->id}: {$batch->totalJobs} jobs");
});

then runs if every job succeeds, catch on the first failure and finally when all have run; without allowFailures() one failure cancels the rest. Of nine orders, order 5 is cancelled, so the log showed first: order 5 is cancelled and finally: 8 processed, and the batch's row read:

The batch's row in job_batchesShell
mysql -t shop -e "SET time_zone = '+00:00';
  SELECT name, total_jobs AS total, pending_jobs AS pending, failed_jobs AS failed,
  FROM_UNIXTIME(created_at) AS created,
  FROM_UNIXTIME(finished_at) AS finished FROM job_batches"
Output
+--------------------+-------+---------+--------+---------------------+----------+
| name               | total | pending | failed | created             | finished |
+--------------------+-------+---------+--------+---------------------+----------+
| September invoices |     9 |       1 |      1 | 2026-09-23 08:58:20 | NULL     |
+--------------------+-------+---------+--------+---------------------+----------+

A failed member still counts as pending. In an earlier run, fixing the order and running queue:retry-batch <id> completed the batch: then fired and finished_at was set, while failed stayed 1. Bus::findBatch($id)->progress() feeds a progress bar.