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. [  Beyond dispatch(): Composing Reliable Async Workflows ](#beyond-codedispatchcode-composing-reliable-async-workflows)
2. [  Job Batching with Bus::batch() ](#job-batching-with-codebusbatchcode)
3. [  Tracking Batch Progress ](#tracking-batch-progress)
4. [  Job Chains for Sequential Pipelines ](#job-chains-for-sequential-pipelines)
5. [  Rate-Limited Job Middleware ](#rate-limited-job-middleware)
6. [  Combining All Three ](#combining-all-three)
7. [  Takeaways ](#takeaways)

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

  #laravel   #queues   #jobs   #async  

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

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

       Table of contents

1. [  01   Beyond dispatch(): Composing Reliable Async Workflows  ](#beyond-codedispatchcode-composing-reliable-async-workflows)
2. [  02   Job Batching with Bus::batch()  ](#job-batching-with-codebusbatchcode)
3. [  03   Tracking Batch Progress  ](#tracking-batch-progress)
4. [  04   Job Chains for Sequential Pipelines  ](#job-chains-for-sequential-pipelines)
5. [  05   Rate-Limited Job Middleware  ](#rate-limited-job-middleware)
6. [  06   Combining All Three  ](#combining-all-three)
7. [  07   Takeaways  ](#takeaways)

 Beyond `dispatch()`: Composing Reliable Async Workflows
-------------------------------------------------------

Most Laravel queue tutorials stop at `SomeJob::dispatch($payload)`. Production systems demand more: coordinated fan-out, sequential pipelines, and throttled integrations with third-party APIs. Laravel ships everything you need — the gap is knowing how the primitives compose.

---

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

Batching lets you dispatch a collection of jobs and react to their collective outcome.

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

$batch = Bus::batch([
    new ProcessInvoice($invoice1),
    new ProcessInvoice($invoice2),
    new ProcessInvoice($invoice3),
])
->then(fn (Batch $batch) => SendBatchSummary::dispatch($batch->id))
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed', [
    'batch' => $batch->id,
    'error' => $e->getMessage(),
]))
->finally(fn (Batch $batch) => $batch->cancelled() ? null : MarkBatchComplete::dispatch())
->name('invoice-processing')
->allowFailures()   // don't cancel the batch on a single failure
->dispatch();

return $batch->id; // store this; poll /api/batches/{id} for progress

```

`allowFailures()` is the key production toggle. Without it, one bad invoice cancels all remaining work. With it, you collect partial results and handle failures in `catch()`.

### Tracking Batch Progress

```php
$batch = Bus::findBatch($batchId);

return [
    'total'     => $batch->totalJobs,
    'processed' => $batch->processedJobs(),
    'failed'    => $batch->failedJobs,
    'finished'  => $batch->finished(),
];

```

Horizon surfaces batch data in its UI automatically — no extra wiring needed.

---

Job Chains for Sequential Pipelines
-----------------------------------

When order matters, chains guarantee step-by-step execution. Each job only runs if the previous one succeeded.

```php
Bus::chain([
    new ValidateOrder($order),
    new ChargePayment($order),
    new FulfillOrder($order),
    new SendConfirmationEmail($order),
])
->catch(fn (Throwable $e) => OrderFailed::dispatch($order, $e->getMessage()))
->dispatch();

```

**Chains and batches compose.** You can nest a batch inside a chain step:

```php
Bus::chain([
    new PrepareExport($report),
    Bus::batch(array_map(
        fn ($chunk) => new ExportChunk($chunk),
        $report->chunks()
    ))->allowFailures(),
    new FinalizeExport($report),
])->dispatch();

```

The chain pauses at the batch step until all batch jobs finish (or the batch is cancelled), then continues to `FinalizeExport`.

---

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

Third-party APIs enforce rate limits. Pushing that logic into a job middleware keeps your job classes clean and makes the constraint reusable.

```php
namespace App\Jobs\Middleware;

use Illuminate\Support\Facades\RateLimiter;

class ThrottleStripeRequests
{
    public function handle(object $job, callable $next): void
    {
        RateLimiter::attempt(
            key: 'stripe-api',
            maxAttempts: 100,          // 100 calls
            callback: fn () => $next($job),
            decaySeconds: 60           // per minute
        ) ?: $job->release(30);        // back off 30 s if limit hit
    }
}

```

Attach it per job:

```php
public function middleware(): array
{
    return [new ThrottleStripeRequests];
}

```

Or use the built-in `RateLimited` middleware with a named limiter defined in `AppServiceProvider`:

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

// Job class
use Illuminate\Queue\Middleware\RateLimited;

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

```

The built-in `RateLimited` middleware automatically re-queues the job with an exponential back-off. Set `$tries` and `$maxExceptions` on the job to bound total retries.

---

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

A real-world pattern: fan out work with a batch, throttle each job against an external API, and chain a finalization step.

```php
Bus::chain([
    new FetchLeads($campaign),
    Bus::batch(
        $campaign->leads->map(fn ($lead) => new EnrichLead($lead))
    )->allowFailures(),
    new ScoreCampaign($campaign),
])->dispatch();

// EnrichLead::middleware()
public function middleware(): array
{
    return [new RateLimited('clearbit')];
}

```

---

Takeaways
---------

- Use `Bus::batch()` for fan-out work; `allowFailures()` is almost always correct in production.
- Store the batch ID immediately — it's your only handle for progress polling.
- Job chains guarantee sequential execution and short-circuit on failure; nest batches inside chains for hybrid workflows.
- Rate-limited middleware belongs in a dedicated class, not inside `handle()` — it's a cross-cutting concern.
- Combine `$tries`, `$backoff`, and `RateLimited` middleware to get safe, self-throttling jobs without custom retry logic.

 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-1&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-1) 

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

  3 questions  

     Q01  What happens to a batch when one job fails and `allowFailures()` is not set?        By default, a single job failure cancels the entire batch. All pending jobs are removed from the queue and the `catch()` callback fires. Setting `allowFailures()` lets remaining jobs continue; failed jobs are counted in `$batch-&gt;failedJobs` and you handle them in `catch()` without stopping the rest. 

      Q02  Can I add jobs to a batch after it has been dispatched?        Yes. From within a batched job you can call `$this-&gt;batch()-&gt;add([new AnotherJob()])`. This is useful for dynamic fan-out where the total work is not known upfront. The batch's `totalJobs` count updates accordingly. 

      Q03  How does the built-in `RateLimited` middleware differ from writing my own?        The built-in `RateLimited` middleware handles re-queuing with exponential back-off automatically and integrates with named limiters defined via `RateLimiter::for()`. A custom middleware gives you full control over back-off duration and release logic, which is useful when an API returns a `Retry-After` header you want to honour precisely. 

  Continue reading

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

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

 [ ![Inertia DevTools Now Available for Firefox](https://cdn.msaied.com/674/445325ab535802b1b68d3adc3ada5cd0.png) Inertia.js DevTools Firefox 

### Inertia DevTools Now Available for Firefox

Inertia DevTools has landed on Firefox with full feature parity to the Chrome extension. Firefox users can now...

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

 17 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/inertia-devtools-now-available-for-firefox) [ ![Laravel Scalpel: Filesystem Intrusion Evidence Scanner for Laravel Apps](https://cdn.msaied.com/675/406b0f123858892b97052502c0020eac.png) security laravel php 

### Laravel Scalpel: Filesystem Intrusion Evidence Scanner for Laravel Apps

Laravel Scalpel is a post-compromise scanner that checks your deployed application's filesystem for rogue PHP...

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

 17 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-scalpel-filesystem-intrusion-evidence-scanner-for-laravel-apps) [ ![Mercure Broadcasting in Laravel 13.32](https://cdn.msaied.com/672/f3bf9ae116410789061208a12ebf2682.png) Laravel Broadcasting Mercure 

### Mercure Broadcasting in Laravel 13.32

Laravel 13.32 ships a native Mercure broadcast driver using SSE, new copyToDisk() and moveToDisk() filesystem...

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

 16 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/mercure-broadcasting-in-laravel-1332) 

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