Laravel Job Batching, Chaining &amp; Rate Limiting | 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)    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/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  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%2Fwww.msaied.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%2Fwww.msaied.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://www.msaied.com/articles) 

 [ ![Laravel Octane + FrankenPHP: Persistent State, Shared Services, and Safe Bootstrapping](https://cdn.msaied.com/224/cc0aa09965b63e7311e93282849ada05.png) laravel octane frankenphp 

### Laravel Octane + FrankenPHP: Persistent State, Shared Services, and Safe Bootstrapping

Running Laravel under FrankenPHP's worker mode unlocks real throughput gains, but persistent state between req...

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

 17 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-octane-frankenphp-persistent-state-shared-services-and-safe-bootstrapping) [ ![Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks](https://cdn.msaied.com/223/2706e56a293bb3b59f39b52956efac09.png) laravel http-client service-container 

### Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks

Stop scattering HTTP client config across service classes. Learn how to build named, pre-configured HTTP clien...

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

 17 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-contextual-http-clients-per-service-config-retries-and-middleware-stacks) [ ![Laravel Signed Routes and Temporary URLs: Secure Link Patterns Beyond the Basics](https://cdn.msaied.com/222/1bcb5bdf8f9b2490d0898cfa28479582.png) laravel security routing 

### Laravel Signed Routes and Temporary URLs: Secure Link Patterns Beyond the Basics

Signed routes are more than a password-reset trick. Learn how to build expressive, tamper-proof URL workflows...

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

 17 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-signed-routes-and-temporary-urls-secure-link-patterns-beyond-the-basics) 

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