Laravel Job Batching, Chaining &amp; Rate Limiting | 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)    Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues        On this page       1. [  Why Basic dispatch() Is Not Enough ](#why-basic-codedispatchcode-is-not-enough)
2. [  Job Batching with Bus::batch() ](#job-batching-with-codebusbatchcode)
3. [  Adding Jobs to a Running Batch ](#adding-jobs-to-a-running-batch)
4. [  Job Chaining with Bus::chain() ](#job-chaining-with-codebuschaincode)
5. [  Rate-Limited Job Middleware ](#rate-limited-job-middleware)
6. [  Custom Backoff on Rate Limit ](#custom-backoff-on-rate-limit)
7. [  Combining All Three ](#combining-all-three)
8. [  Key Takeaways ](#key-takeaways)

  ![Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues](https://cdn.msaied.com/515/f0ef4270f8cb79ceff107c7a7c63f1ed.png)

  #laravel   #queues   #jobs   #horizon   #async  

 Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues 
=======================================================================

     6 Aug 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   Why Basic dispatch() Is Not Enough  ](#why-basic-codedispatchcode-is-not-enough)
2. [  02   Job Batching with Bus::batch()  ](#job-batching-with-codebusbatchcode)
3. [  03   Adding Jobs to a Running Batch  ](#adding-jobs-to-a-running-batch)
4. [  04   Job Chaining with Bus::chain()  ](#job-chaining-with-codebuschaincode)
5. [  05   Rate-Limited Job Middleware  ](#rate-limited-job-middleware)
6. [  06   Custom Backoff on Rate Limit  ](#custom-backoff-on-rate-limit)
7. [  07   Combining All Three  ](#combining-all-three)
8. [  08   Key Takeaways  ](#key-takeaways)

 Why Basic `dispatch()` Is Not Enough
------------------------------------

Single-job dispatching works fine for isolated tasks, but real SaaS workloads demand coordination: import a CSV, notify each row's owner, then send a summary email. Get any step wrong and you want partial retries — not a full restart. Laravel's batch and chain APIs, combined with rate-limited job middleware, give you that control.

---

Job Batching with `Bus::batch()`
--------------------------------

Batches let you dispatch a collection of jobs and react when the whole set finishes, partially fails, or is cancelled.

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

$batch = Bus::batch([
    new ProcessRowJob($row) for $row in $rows, // spread or array
])
->then(fn (Batch $batch) => SummaryMail::dispatch($batch->id))
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed', [
    'batch' => $batch->id,
    'error' => $e->getMessage(),
]))
->finally(fn (Batch $batch) => BatchCompleted::dispatch($batch->id))
->name('csv-import')
->allowFailures()   // keep running even if some jobs fail
->dispatch();

```

> `allowFailures()` is critical for large imports: one bad row should not cancel 10,000 others.

Track progress in Filament or a dashboard via `$batch->progress()`, `$batch->failedJobs`, and `$batch->pendingJobs`.

### Adding Jobs to a Running Batch

Inside a batched job you can append more work — useful for tree-shaped workloads:

```php
public function handle(): void
{
    $this->batch()->add([
        new ProcessChildJob($this->parentId, $child)
        foreach ($this->children() as $child),
    ]);
}

```

---

Job Chaining with `Bus::chain()`
--------------------------------

Chains enforce strict sequential execution. If any job fails, the rest are abandoned.

```php
Bus::chain([
    new VerifyPayment($orderId),
    new FulfillOrder($orderId),
    new SendConfirmationEmail($orderId),
])
->catch(fn (Throwable $e) => Order::fail($orderId, $e->getMessage()))
->dispatch();

```

You can mix batches inside chains for fan-out/fan-in patterns:

```php
Bus::chain([
    new PrepareImport($fileId),
    Bus::batch($rowJobs)->allowFailures(),
    new FinaliseImport($fileId),
])->dispatch();

```

This runs `PrepareImport`, then all row jobs in parallel, then `FinaliseImport` — a powerful pattern for ETL pipelines.

---

Rate-Limited Job Middleware
---------------------------

Throttling at the job level prevents hammering third-party APIs regardless of how many workers you run.

```php
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Support\Facades\RateLimiter;

// AppServiceProvider::boot()
RateLimiter::for('stripe', fn () =>
    Limit::perMinute(100)->by('stripe-api')
);

// Inside the job
public function middleware(): array
{
    return [new RateLimited('stripe')];
}

```

When the limit is hit, the job is **automatically released back** to the queue with an exponential backoff — no manual `$this->release()` needed.

### Custom Backoff on Rate Limit

```php
use Illuminate\Queue\Middleware\RateLimitedWithRedis;

public function middleware(): array
{
    return [(new RateLimitedWithRedis('stripe'))->dontRelease()];
    // dontRelease() deletes the job instead of re-queuing — use carefully
}

```

`RateLimitedWithRedis` uses atomic Lua scripts for precise per-second limits, making it safer under Horizon's multi-worker concurrency.

---

Combining All Three
-------------------

A production import pipeline might look like:

```php
Bus::chain([
    new ValidateFile($fileId),                        // sequential
    Bus::batch($parseJobs)->allowFailures(),           // parallel parse
    Bus::batch($enrichJobs)->allowFailures(),          // parallel API calls (rate-limited)
    new GenerateReport($fileId),                      // sequential
])->dispatch();

```

Each `enrichJob` carries `RateLimited('external-api')` middleware, so the batch fans out as fast as the limiter allows without a single line of throttle logic in the business code.

---

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

- Use **`Bus::batch()`** for parallel fan-out; use **`allowFailures()`** for fault-tolerant imports.
- Use **`Bus::chain()`** for strict sequential steps; nest batches inside chains for fan-out/fan-in.
- Attach **`RateLimited`** middleware at the job level — it survives worker restarts and scales across all Horizon processes.
- Prefer **`RateLimitedWithRedis`** over the plain variant when you need sub-second precision.
- Track batch state via `$batch->progress()` for real-time dashboards without polling your database directly.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fjob-batching-chaining-and-rate-limited-middleware-in-laravel-queues-4&text=Job+Batching%2C+Chaining%2C+and+Rate-Limited+Middleware+in+Laravel+Queues) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fjob-batching-chaining-and-rate-limited-middleware-in-laravel-queues-4) 

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

  3 questions  

     Q01  What happens to a batch when one job throws an exception and `allowFailures()` is set?        The failed job is recorded in `job_batches.failed_jobs` and the `catch` callback fires, but the remaining pending jobs continue processing. The batch only moves to `finally` once all jobs have either completed or failed. 

      Q02  Can I use `RateLimited` middleware with batched jobs?        Yes. Each job in a batch is an independent queue message, so middleware is applied per-job. Rate-limited jobs are released back to the queue and retried, which may slow overall batch completion but will not cancel the batch. 

      Q03  How do I prevent a chain from silently swallowing failures?        Always attach a `-&gt;catch()` callback to `Bus::chain()`. Without it, a failed job abandons the rest of the chain with no notification. The callback receives the `Throwable` so you can alert, compensate, or update domain state. 

  Continue reading

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

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

 [ ![Filament v4.13.3 Released: Bug Fixes, MFA Improvements, and New Translations](https://cdn.msaied.com/685/d166e49051b110828607993a8719536e.png) filament laravel php 

### Filament v4.13.3 Released: Bug Fixes, MFA Improvements, and New Translations

Filament v4.13.3 ships with a dozen targeted fixes covering MFA password checks, FileUpload audio previews, qu...

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

 20 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4133-released-bug-fixes-mfa-improvements-and-new-translations) [ ![Laravel Reverb in Production: Scaling WebSockets, Auth Channels, and Presence at Load](https://cdn.msaied.com/684/1c8fcd52449a6596e09ec043b245955f.png) laravel websockets reverb 

### Laravel Reverb in Production: Scaling WebSockets, Auth Channels, and Presence at Load

Reverb ships as Laravel's first-party WebSocket server. This guide covers production deployment, horizontal sc...

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

 20 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-reverb-in-production-scaling-websockets-auth-channels-and-presence-at-load) [ ![Filament v4 Render Hooks: Injecting UI Into Any Panel Layer Without Hacks](https://cdn.msaied.com/683/e6350724743c14481d11da6bd38e44e2.png) filament laravel filament-v4 

### Filament v4 Render Hooks: Injecting UI Into Any Panel Layer Without Hacks

Render hooks let you inject Blade or Livewire content into specific Filament panel slots without overriding co...

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

 20 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-render-hooks-injecting-ui-into-any-panel-layer-without-hacks) 

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