0

Job retries, backoff, and max attempts

Intermediate5 min read·lv-19-004
interview

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
<?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(),
]);