Job retries, backoff, and max attempts
Concept
Job chaining runs a series of jobs sequentially — the next job in the chain starts only after the previous one completes successfully. If any job fails, the remaining jobs in the chain are NOT executed.
Bus::chain([...]): Creates a chain from an array of job instances. Dispatch with ->dispatch().
Job batching (Bus::batch([...]))**: Groups multiple jobs that run in parallel. Provides progress tracking, completion/failure callbacks, and the ability to cancel the batch.
Batch features:
then(callable $callback): Runs after all batch jobs complete successfully.catch(callable $callback): Runs if any job in the batch fails (can fire multiple times).finally(callable $callback): Runs after all jobs finish, regardless of success/failure.$batch->progress(): Percentage complete.$batch->finished(): True if all jobs done.$batch->cancelled(): True if batch was cancelled.$batch->cancel(): Cancel remaining jobs.
Batch DB table: php artisan queue:batches-table && php artisan migrate creates job_batches.
WithBatchId and Batchable trait: Add use Batchable to jobs that participate in batches. Access $this->batch() to get the current batch inside the job.
Chains within batches: Each item in Bus::batch([]) can be an array (sequential chain within the parallel batch).
Code Example
<?php
use Illuminate\Support\Facades\Bus;
// Job chain — sequential, stops on failure
Bus::chain([
new ValidatePayment($order),
new ChargeCustomer($order),
new FulfillOrder($order),
new SendConfirmationEmail($order),
])->dispatch();
// Chain with catch handler
Bus::chain([
new ValidatePayment($order),
new ChargeCustomer($order),
])->catch(function(\Throwable $e) use ($order) {
$order->update(['status' => 'payment_failed']);
\Illuminate\Support\Facades\Mail::to($order->customer)->send(new PaymentFailedMail($order));
})->dispatch();
// Batch — parallel execution
// php artisan queue:batches-table && php artisan migrate
$batch = Bus::batch([
new ProcessUserReport(1),
new ProcessUserReport(2),
new ProcessUserReport(3),
// ... 100 jobs running in parallel
])
->then(function(\Illuminate\Bus\Batch $batch) {
// All jobs succeeded
\Illuminate\Support\Facades\Log::info('All reports processed', [
'batch_id' => $batch->id,
'total' => $batch->totalJobs,
]);
})
->catch(function(\Illuminate\Bus\Batch $batch, \Throwable $e) {
\Illuminate\Support\Facades\Log::error('Batch job failed', [
'batch_id' => $batch->id,
'failed' => $batch->failedJobs,
]);
})
->finally(function(\Illuminate\Bus\Batch $batch) {
// Always runs — notify admin of completion/status
})
->allowFailures() // don't cancel entire batch if one job fails
->dispatch();
// Job with Batchable — aware of its batch
class ProcessUserReport implements \Illuminate\Contracts\Queue\ShouldQueue
{
use \Illuminate\Bus\Batchable, \Illuminate\Bus\Queueable;
use \Illuminate\Foundation\Bus\Dispatchable, \Illuminate\Queue\SerializesModels;
public function handle(): void
{
if ($this->batch()->cancelled()) {
return; // batch was cancelled — skip this job
}
// ... generate report ...
}
}
// Batch progress in controller
$batch = Bus::findBatch($batchId);
return response()->json([
'progress' => $batch->progress(),
'finished' => $batch->finished(),
'cancelled' => $batch->cancelled(),
]);