The Problem with Naive Job Dispatch
Most Laravel applications start with simple dispatch(new SomeJob($id)) calls scattered through controllers. That works until you need to process 50,000 records, coordinate dependent steps, or know when a multi-step workflow has truly finished. Laravel's batch and chain APIs solve this — but only if you understand their failure semantics.
Batches vs Chains: Know the Difference
A chain is a sequential pipeline: each job runs only after the previous one succeeds. A batch is a parallel fan-out: all jobs run concurrently, and you get callbacks when they collectively finish or partially fail.
Use chains when step B depends on step A's output. Use batches when you can process N items independently and want to react to completion.
// Chain: sequential dependency
bus()->chain([
new ValidateImport($importId),
new TransformRows($importId),
new NotifyUser($importId),
])->dispatch();
// Batch: parallel fan-out
$batch = Bus::batch(
$rowIds->map(fn ($id) => new ProcessRow($id))->all()
)
->then(fn (Batch $batch) => ImportCompleted::dispatch($batch->id))
->catch(fn (Batch $batch, Throwable $e) => ImportFailed::dispatch($batch->id, $e->getMessage()))
->finally(fn (Batch $batch) => Cache::forget("import:{$batch->id}:lock"))
->name('row-import')
->allowFailures() // don't cancel remaining jobs on first failure
->dispatch();
allowFailures() Is Not Optional in Real Systems
By default, a single failed job cancels the entire batch. For bulk imports or notification fans, that's almost never what you want. Call ->allowFailures() and inspect $batch->failedJobs in your then callback to decide what to do with partial success.
Nesting Batches Inside Chains
You can add a batch as a step inside a chain using Bus::chain with a Bus::batch call embedded:
Bus::chain([
new PrepareImport($importId),
Bus::batch(
$chunks->map(fn ($chunk) => new ProcessChunk($importId, $chunk))->all()
)->allowFailures(),
new FinaliseImport($importId),
])->dispatch();
The chain pauses at the batch step and only advances to FinaliseImport once every batch job has settled (succeeded or failed, depending on allowFailures).
Catch Callbacks and Error Observability
The catch callback fires on the first job failure in a batch. It receives the Batch model and the Throwable. Use it to record structured failure context rather than relying on the generic failed-jobs table alone:
->catch(function (Batch $batch, Throwable $e) use ($importId) {
ImportAttempt::where('batch_id', $batch->id)->update([
'status' => 'partial_failure',
'error' => $e->getMessage(),
'failed_count' => $batch->failedJobs,
]);
})
For chains, attach a catch directly on the chain dispatch:
Bus::chain([...])
->catch(fn (Throwable $e) => Log::critical('Import chain failed', ['error' => $e->getMessage()]))
->dispatch();
Pruning Stale Batch Records
Every dispatched batch writes a row to job_batches. In high-throughput systems this table grows fast. Schedule the built-in prune command:
// routes/console.php or Kernel.php
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();
Inspecting Batch Progress in Real Time
The Batch model exposes totalJobs, processedJobs(), failedJobs, and progress() (0–100). Poll it from a Livewire component or a simple API endpoint to build a progress bar without any additional infrastructure:
$batch = Bus::findBatch($batchId);
return [
'progress' => $batch->progress(),
'finished' => $batch->finished(),
'failed' => $batch->failedJobs,
];
Key Takeaways
- Use chains for sequential dependencies; use batches for parallel fan-out with collective callbacks.
- Always call
->allowFailures()on batches that process bulk data — partial success is usually acceptable. - The
catchcallback fires once per batch failure event; use it for structured observability, not just logging. - Nest a
Bus::batch()inside aBus::chain()to combine parallel processing with sequential orchestration. - Schedule
queue:prune-batchesto prevent thejob_batchestable from becoming a performance liability. - Expose
$batch->progress()directly from the database model — no Redis counters needed for simple UIs.