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

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

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

  [ home ](https://msaied.com)    [ articles ](https://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%2Fmsaied.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%2Fmsaied.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://msaied.com/articles) 

 [ ![Cursor Pagination, Chunked Iteration, and Lazy Collections at Scale in Laravel](https://cdn.msaied.com/292/a09cfdc4dcc65660fb6ada3aae3fa264.png) laravel eloquent performance 

### Cursor Pagination, Chunked Iteration, and Lazy Collections at Scale in Laravel

Offset pagination breaks under large datasets. Learn how cursor pagination, chunked iteration, and lazy collec...

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

 25 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/cursor-pagination-chunked-iteration-and-lazy-collections-at-scale-in-laravel) [ ![FrankenPHP, OPcache JIT, and Preloading: Maximising Laravel Throughput](https://cdn.msaied.com/289/b7db80720c9f35a8631e34515666e691.png) laravel frankenphp opcache 

### FrankenPHP, OPcache JIT, and Preloading: Maximising Laravel Throughput

A practical deep-dive into running Laravel under FrankenPHP with OPcache JIT and preloading enabled — covering...

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

 25 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/frankenphp-opcache-jit-and-preloading-maximising-laravel-throughput) [ ![Laravel Queues: Job Middleware for Idempotency, Rate Limiting, and Graceful Failure](https://cdn.msaied.com/288/46c6bf48663538a47ca37b0655c41674.png) laravel queues job-middleware 

### Laravel Queues: Job Middleware for Idempotency, Rate Limiting, and Graceful Failure

Beyond basic dispatching, Laravel job middleware lets you enforce idempotency, apply fine-grained rate limits,...

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

 25 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-queues-job-middleware-for-idempotency-rate-limiting-and-graceful-failure) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://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://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://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)
