Laravel Job Batching, Chaining &amp; Rate Limiting | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Job Batching, Chaining, and Rate-Limited Middleware in Laravel Queues

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

 Go beyond basic dispatching: learn how to compose Laravel job batches, build resilient chains, and throttle throughput with rate-limited middleware — all with production-ready patterns.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 6 Aug 2026 · Updated 6 Aug 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [queues](https://www.msaied.com/public/articles?search=queues)
- [jobs](https://www.msaied.com/public/articles?search=jobs)
- [horizon](https://www.msaied.com/public/articles?search=horizon)
- [async](https://www.msaied.com/public/articles?search=async)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

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

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

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleExtract an Image's Dominant Color in Laravel 13.24](https://www.msaied.com/public/articles/extract-an-images-dominant-color-in-laravel-1324) [Next articleManaged Queues: Autoscaling Queue Workers on Laravel Cloud](https://www.msaied.com/public/articles/managed-queues-autoscaling-queue-workers-on-laravel-cloud)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
