Laravel Job Batching &amp; Chaining for Async Workflows | 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)    Laravel Job Batching and Chaining: Coordinating Complex Async Workflows        On this page       1. [  Why Simple Queued Jobs Are Not Enough ](#why-simple-queued-jobs-are-not-enough)
2. [  Job Chaining: Sequential Guarantees ](#job-chaining-sequential-guarantees)
3. [  Passing State Between Chained Jobs ](#passing-state-between-chained-jobs)
4. [  Job Batching: Fan-Out with a Finish Line ](#job-batching-fan-out-with-a-finish-line)
5. [  Allowing Partial Failure ](#allowing-partial-failure)
6. [  Combining Batches and Chains ](#combining-batches-and-chains)
7. [  Pruning the job\_batches Table ](#pruning-the-codejob-batchescode-table)
8. [  Monitoring Batch Progress ](#monitoring-batch-progress)
9. [  Key Takeaways ](#key-takeaways)

  ![Laravel Job Batching and Chaining: Coordinating Complex Async Workflows](https://cdn.msaied.com/290/3b69072ea0f13031737fb4104c720593.png)

  #laravel   #queues   #async   #job-batching   #backend  

 Laravel Job Batching and Chaining: Coordinating Complex Async Workflows 
=========================================================================

     25 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

  9 sections  

1. [  01   Why Simple Queued Jobs Are Not Enough  ](#why-simple-queued-jobs-are-not-enough)
2. [  02   Job Chaining: Sequential Guarantees  ](#job-chaining-sequential-guarantees)
3. [  03   Passing State Between Chained Jobs  ](#passing-state-between-chained-jobs)
4. [  04   Job Batching: Fan-Out with a Finish Line  ](#job-batching-fan-out-with-a-finish-line)
5. [  05   Allowing Partial Failure  ](#allowing-partial-failure)
6. [  06   Combining Batches and Chains  ](#combining-batches-and-chains)
7. [  07   Pruning the job\_batches Table  ](#pruning-the-codejob-batchescode-table)
8. [  08   Monitoring Batch Progress  ](#monitoring-batch-progress)
9. [  09   Key Takeaways  ](#key-takeaways)

       Why Simple Queued Jobs Are Not Enough
-------------------------------------

A single `dispatch(new ProcessInvoice($id))` gets you far, but real SaaS workflows are rarely one step. You need to import a CSV, validate each row, enrich records via an external API, then notify the user — and you need to know when *all of it* finishes, or which part failed.

Laravel's `Bus::batch()` and job chaining solve exactly this, but the nuances around failure handling, nested batches, and state propagation trip up even experienced engineers.

---

Job Chaining: Sequential Guarantees
-----------------------------------

Chaining enforces order. Each job runs only if the previous one succeeded.

```php
use Illuminate\Support\Facades\Bus;

Bus::chain([
    new ValidateImport($importId),
    new EnrichRecords($importId),
    new NotifyUser($importId),
])->onQueue('imports')->dispatch();

```

If `EnrichRecords` throws, `NotifyUser` never runs. The chain is stored in the job payload itself — no extra database row. That simplicity is also a limitation: you cannot inspect chain progress from outside.

### Passing State Between Chained Jobs

Avoid coupling jobs through shared mutable state in the database when you can pass identifiers instead. Each job re-queries what it needs:

```php
class EnrichRecords implements ShouldQueue
{
    public function __construct(private readonly int $importId) {}

    public function handle(ImportRepository $repo): void
    {
        $import = $repo->findOrFail($this->importId);
        // enrich and persist
    }
}

```

This keeps jobs idempotent and safe to retry.

---

Job Batching: Fan-Out with a Finish Line
----------------------------------------

Batches let you dispatch many jobs in parallel and react when they all complete — or when any fails.

```php
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;

$batch = Bus::batch(
    $importRows->map(fn ($row) => new ProcessRow($row))->all()
)
->then(fn (Batch $batch) => NotifyImportComplete::dispatch($batch->id))
->catch(fn (Batch $batch, Throwable $e) => ImportFailed::dispatch($batch->id, $e->getMessage()))
->finally(fn (Batch $batch) => Import::markFinished($batch->id))
->onQueue('imports')
->dispatch();

$importId = $batch->id; // store for status polling

```

`then` fires once when *all* jobs succeed. `catch` fires on the *first* failure. `finally` always fires. These callbacks are serialized closures stored in the `job_batches` table — keep them small and side-effect-free.

### Allowing Partial Failure

By default, one failed job cancels the batch. For bulk operations where partial success is acceptable:

```php
Bus::batch($jobs)
    ->allowFailures()
    ->then(fn (Batch $b) => $this->summarize($b))
    ->dispatch();

```

Inside `then` you can inspect `$batch->failedJobs` to report which rows failed without aborting the whole import.

---

Combining Batches and Chains
----------------------------

The real power emerges when you nest them. Run a batch of parallel jobs, then chain a sequential step after all of them finish:

```php
Bus::chain([
    new PrepareImport($importId),
    Bus::batch(
        $rows->map(fn ($r) => new ProcessRow($r))->all()
    )->allowFailures(),
    new FinalizeImport($importId),
])->dispatch();

```

The chain pauses at the batch step until the batch resolves, then continues to `FinalizeImport`. This pattern handles fan-out/fan-in without any custom orchestration code.

---

Pruning the `job_batches` Table
-------------------------------

Batches accumulate rows. Add the prune command to your scheduler:

```php
// routes/console.php
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')
    ->daily();

```

`--unfinished` prunes batches that never completed — important for catching leaked batches from deploy-time failures.

---

Monitoring Batch Progress
-------------------------

Expose a lightweight status endpoint for the frontend:

```php
public function status(string $batchId): JsonResponse
{
    $batch = Bus::findBatch($batchId);

    return response()->json([
        'progress'  => $batch->progress(),
        'finished'  => $batch->finished(),
        'failed'    => $batch->failedJobs,
        'cancelled' => $batch->cancelled(),
    ]);
}

```

`$batch->progress()` returns an integer 0–100. Poll this from a Livewire component or Alpine.js interval for a real-time progress bar without WebSockets.

---

Key Takeaways
-------------

- Use **chains** for sequential steps where each depends on the previous succeeding.
- Use **batches** for parallel fan-out where you need a collective finish line.
- **Nest** a batch inside a chain to combine both patterns cleanly.
- Call `allowFailures()` on bulk operations; inspect `failedJobs` in `then`.
- Keep batch callbacks minimal — they are serialized closures, not service-container-aware by default.
- Schedule `queue:prune-batches` to prevent unbounded table growth.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-job-batching-and-chaining-coordinating-complex-async-workflows&text=Laravel+Job+Batching+and+Chaining%3A+Coordinating+Complex+Async+Workflows) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-job-batching-and-chaining-coordinating-complex-async-workflows) 

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

  3 questions  

     Q01  Can a batch callback dispatch another batch or chain?        Yes. The `then`, `catch`, and `finally` callbacks run in a queue worker context, so you can dispatch new jobs or batches from them. Keep the callbacks thin — offload heavy logic to a dedicated job dispatched from within the callback. 

      Q02  What happens to a batch when a worker restarts mid-processing?        Each job in the batch is an independent queue message. If a worker restarts, unprocessed jobs remain in the queue and are picked up by the next available worker. The batch row tracks counts atomically, so progress is not lost. Failed jobs increment `failed_jobs` on the batch row. 

      Q03  Is there a limit to how many jobs I can put in a single batch?        There is no hard framework limit, but very large batches (tens of thousands of jobs) can cause slow dispatch because all job records are inserted in one transaction. Chunk your batch dispatch in groups of a few hundred using `Bus::batch($chunk)-&gt;dispatch()` inside a loop, each as a separate batch, then coordinate them with an outer chain or a custom aggregator job. 

  Continue reading

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

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

 [ ![Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](https://cdn.msaied.com/295/d977bd189583149245c03d6d763d9db5.png) laravel database performance 

### Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel

Learn how Laravel's database layer handles read/write splitting, when sticky reads matter, and how to layer Pg...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel-2) [ ![Laravel Service Container: Contextual Binding, Tagging, and Method Injection](https://cdn.msaied.com/294/e5b9d047bd33c3f8b80069ef6a178884.png) laravel service-container dependency-injection 

### Laravel Service Container: Contextual Binding, Tagging, and Method Injection

Go beyond basic binding. Learn how contextual binding resolves different implementations per consumer, how tag...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-service-container-contextual-binding-tagging-and-method-injection-1) [ ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain](https://cdn.msaied.com/293/f392a5ea52536901eac9677ffa2d070d.png) laravel postgresql eloquent 

### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain

JSONB columns unlock flexible schemas, but most Laravel apps leave performance on the table. Learn how to inde...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 25 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-pain) 

   [  ![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)
