Laravel Job Batching &amp; Chaining Deep Dive | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Job Batching, Chaining, and Catch Callbacks: Reliable Async Workflows in Laravel        On this page       1. [  The Problem with Naive Job Dispatch ](#the-problem-with-naive-job-dispatch)
2. [  Batches vs Chains: Know the Difference ](#batches-vs-chains-know-the-difference)
3. [  allowFailures() Is Not Optional in Real Systems ](#codeallowfailurescode-is-not-optional-in-real-systems)
4. [  Nesting Batches Inside Chains ](#nesting-batches-inside-chains)
5. [  Catch Callbacks and Error Observability ](#catch-callbacks-and-error-observability)
6. [  Pruning Stale Batch Records ](#pruning-stale-batch-records)
7. [  Inspecting Batch Progress in Real Time ](#inspecting-batch-progress-in-real-time)
8. [  Key Takeaways ](#key-takeaways)

  ![Job Batching, Chaining, and Catch Callbacks: Reliable Async Workflows in Laravel](https://cdn.msaied.com/666/f8aaa3879dc7efd419291ffa3e0b15c1.png)

  #laravel   #queues   #async   #jobs   #batching  

 Job Batching, Chaining, and Catch Callbacks: Reliable Async Workflows in Laravel 
==================================================================================

     13 Sep 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   The Problem with Naive Job Dispatch  ](#the-problem-with-naive-job-dispatch)
2. [  02   Batches vs Chains: Know the Difference  ](#batches-vs-chains-know-the-difference)
3. [  03   allowFailures() Is Not Optional in Real Systems  ](#codeallowfailurescode-is-not-optional-in-real-systems)
4. [  04   Nesting Batches Inside Chains  ](#nesting-batches-inside-chains)
5. [  05   Catch Callbacks and Error Observability  ](#catch-callbacks-and-error-observability)
6. [  06   Pruning Stale Batch Records  ](#pruning-stale-batch-records)
7. [  07   Inspecting Batch Progress in Real Time  ](#inspecting-batch-progress-in-real-time)
8. [  08   Key Takeaways  ](#key-takeaways)

 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.

```php
// Chain: sequential dependency
bus()->chain([
    new ValidateImport($importId),
    new TransformRows($importId),
    new NotifyUser($importId),
])->dispatch();

```

```php
// 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:

```php
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:

```php
->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:

```php
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:

```php
// 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:

```php
$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 `catch` callback fires once per batch failure event; use it for structured observability, not just logging.
- Nest a `Bus::batch()` inside a `Bus::chain()` to combine parallel processing with sequential orchestration.
- Schedule `queue:prune-batches` to prevent the `job_batches` table from becoming a performance liability.
- Expose `$batch->progress()` directly from the database model — no Redis counters needed for simple UIs.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fjob-batching-chaining-and-catch-callbacks-reliable-async-workflows-in-laravel&text=Job+Batching%2C+Chaining%2C+and+Catch+Callbacks%3A+Reliable+Async+Workflows+in+Laravel) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fjob-batching-chaining-and-catch-callbacks-reliable-async-workflows-in-laravel) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does the `catch` callback in a batch fire for every failed job or just the first?        It fires on the first failure within the batch. If you need per-job failure handling, implement the `failed(Throwable $e)` method directly on the job class alongside the batch-level catch callback. 

      Q02  Can I add more jobs to a batch after it has been dispatched?        Yes. Call `Bus::findBatch($batchId)-&gt;add([new AnotherJob()])` at any point before the batch finishes. This is useful when a job discovers additional work that should belong to the same batch. 

      Q03  What happens to the chain if a nested batch has `allowFailures()` and some jobs fail?        The chain still advances to the next step because `allowFailures()` prevents the batch from being marked as cancelled. Inspect `$batch-&gt;failedJobs` in the `then` callback or the next chain step to decide whether to proceed or abort. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Partial Indexes and Covering Indexes in PostgreSQL: A Laravel Developer's Guide](https://cdn.msaied.com/665/ced6904aad758906b6047d70ea25e267.png) postgresql laravel performance 

### Partial Indexes and Covering Indexes in PostgreSQL: A Laravel Developer's Guide

Learn how partial and covering indexes eliminate wasted index space and redundant heap fetches in Laravel apps...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 13 Sep 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/partial-indexes-and-covering-indexes-in-postgresql-a-laravel-developers-guide-1) [ ![Filament v4 at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning](https://cdn.msaied.com/664/be327447c5231a3cb27a5df9597890dd.png) filament laravel multi-panel 

### Filament v4 at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning

Running Filament v4 across multiple panels with distinct auth guards and thousands of rows? This guide covers...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 13 Sep 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v4-at-scale-multi-panel-auth-custom-panels-and-table-query-tuning) [ ![Macros, Mixins, and Custom Collection Methods in Laravel](https://cdn.msaied.com/663/b8e39b17d427358aa43b5c3e8c1be908.png) laravel collections macros 

### Macros, Mixins, and Custom Collection Methods in Laravel

Learn how to extend Laravel's core classes with macros, mixins, and custom Collection methods — keeping your c...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 13 Sep 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/macros-mixins-and-custom-collection-methods-in-laravel-2) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
