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 Complex Queue Workflows ](#beyond-codedispatchcode-composing-complex-queue-workflows)
2. [  Job Batching ](#job-batching)
3. [  Adding Jobs to a Running Batch ](#adding-jobs-to-a-running-batch)
4. [  Job Chaining ](#job-chaining)
5. [  Mixing Batches Inside Chains ](#mixing-batches-inside-chains)
6. [  Rate-Limited Job Middleware ](#rate-limited-job-middleware)
7. [  Takeaways ](#takeaways)

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

  #laravel   #queues   #jobs   #async  

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

     30 Jul 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Beyond dispatch(): Composing Complex Queue Workflows  ](#beyond-codedispatchcode-composing-complex-queue-workflows)
2. [  02   Job Batching  ](#job-batching)
3. [  03   Adding Jobs to a Running Batch  ](#adding-jobs-to-a-running-batch)
4. [  04   Job Chaining  ](#job-chaining)
5. [  05   Mixing Batches Inside Chains  ](#mixing-batches-inside-chains)
6. [  06   Rate-Limited Job Middleware  ](#rate-limited-job-middleware)
7. [  07   Takeaways  ](#takeaways)

 Beyond `dispatch()`: Composing Complex Queue Workflows
------------------------------------------------------

Most Laravel queue tutorials stop at `dispatch(MyJob::class)`. Production systems need more: fan-out work across hundreds of records, guarantee sequential steps, and respect third-party API rate limits — all without hand-rolling a state machine. Laravel's batch, chain, and middleware primitives cover every one of those cases cleanly.

---

Job Batching
------------

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 ProcessInvoice($invoice1),
    new ProcessInvoice($invoice2),
    new ProcessInvoice($invoice3),
])
->then(fn (Batch $batch) => Report::markComplete($batch->id))
->catch(fn (Batch $batch, Throwable $e) => Report::markFailed($batch->id, $e->getMessage()))
->finally(fn (Batch $batch) => Cache::forget("batch:{$batch->id}"))
->allowFailures()   // don't cancel remaining jobs on first failure
->dispatch();

session(['batch_id' => $batch->id]);

```

`allowFailures()` is critical for bulk-import scenarios: one bad row should not abort 10,000 others. Poll `Bus::findBatch($id)` to expose progress to the UI.

### Adding Jobs to a Running Batch

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

```php
public function handle(): void
{
    $children = $this->node->children;

    if ($children->isNotEmpty()) {
        $this->batch()->add(
            $children->map(fn ($child) => new ProcessNode($child))
        );
    }

    $this->node->markProcessed();
}

```

The batch's `totalJobs` counter updates atomically, so `then()` only fires once every added job has also completed.

---

Job Chaining
------------

Chains enforce strict ordering: job N+1 only runs if job N succeeds.

```php
Bus::chain([
    new ValidateOrder($order),
    new ChargePayment($order),
    new FulfillOrder($order),
    new SendConfirmationEmail($order),
])
->catch(function (Throwable $e) use ($order) {
    $order->markFailed($e->getMessage());
    Notification::send($order->owner, new OrderFailedNotification($order));
})
->dispatch();

```

**Key distinction:** a chain's `catch` fires on the *first* failure and the remaining jobs are discarded. If you need partial completion semantics, use a batch instead.

### Mixing Batches Inside Chains

You can nest a batch as one step in a chain:

```php
Bus::chain([
    new PrepareExport($report),
    Bus::batch([
        new ExportChunk($report, 0),
        new ExportChunk($report, 1),
        new ExportChunk($report, 2),
    ]),
    new FinaliseExport($report),
])->dispatch();

```

The chain pauses at the batch step and only advances to `FinaliseExport` once every chunk job completes successfully.

---

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

Third-party APIs impose rate limits. The cleanest solution is a per-job middleware that uses Redis to throttle throughput without blocking a worker thread.

```php
namespace App\Jobs\Middleware;

use Closure;
use Illuminate\Support\Facades\Redis;

class ThrottleWithRedis
{
    public function __construct(
        private readonly string $key,
        private readonly int $maxAttempts,
        private readonly int $decaySeconds,
    ) {}

    public function handle(object $job, Closure $next): void
    {
        Redis::throttle($this->key)
            ->allow($this->maxAttempts)
            ->every($this->decaySeconds)
            ->then(
                fn () => $next($job),
                function () use ($job) {
                    $job->release(10); // re-queue after 10 s
                }
            );
    }
}

```

Attach it in the job class:

```php
public function middleware(): array
{
    return [
        new ThrottleWithRedis('stripe-api', 80, 60),
    ];
}

```

Laravel ships `Illuminate\Queue\Middleware\RateLimited` and `RateLimitedWithRedis` out of the box, backed by the same `RateLimiter` facade used for HTTP routes — so you can share a named limiter:

```php
// AppServiceProvider
RateLimiter::for('stripe', fn () => Limit::perMinute(80));

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

```

---

Takeaways
---------

- Use **batches** when jobs are independent and you need aggregate callbacks; use **chains** when order and dependency matter.
- `allowFailures()` on a batch prevents one bad job from cancelling the rest — essential for bulk operations.
- Nest a batch inside a chain to get fan-out parallelism with a sequential before/after step.
- Rate-limited middleware keeps workers non-blocking: `release()` re-queues the job rather than sleeping the process.
- Share `RateLimiter` definitions between HTTP and queue layers to enforce a single source of truth for API quotas.

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

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

  3 questions  

     Q01  What happens to a batch if one job throws an exception?        By default the batch is cancelled and remaining pending jobs are not executed. Call `allowFailures()` when dispatching to let other jobs continue; the `catch` callback still fires for each failed job, and `failedJobs` is incremented on the batch record. 

      Q02  Does `RateLimited` middleware count against the job's `tries` limit?        No — releasing a job via `$job-&gt;release()` inside middleware does not increment the attempt counter. Only a thrown exception or an explicit `$job-&gt;fail()` counts as an attempt, so throttled jobs can be re-queued many times without being discarded. 

      Q03  Can I track batch progress in a Filament or Livewire UI?        Yes. Store the batch ID in the database or session after dispatch, then poll `Bus::findBatch($id)` on a Livewire component using a `wire:poll` directive. The returned `Batch` object exposes `progress()`, `totalJobs`, `processedJobs`, and `failedJobs` properties. 

  Continue reading

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

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

 [ ![Laravel Observers vs. Model Events: Choosing the Right Hook for Domain Side-Effects](https://cdn.msaied.com/488/451564d0a43aa33809619fa299027222.png) laravel eloquent domain-events 

### Laravel Observers vs. Model Events: Choosing the Right Hook for Domain Side-Effects

Model events and observers look similar but behave differently under bulk operations, transactions, and test i...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-observers-vs-model-events-choosing-the-right-hook-for-domain-side-effects) [ ![Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines](https://cdn.msaied.com/487/2db17c4f7927d4a229a4786c03e7b67b.png) laravel ai pgvector 

### Practical RAG in Laravel: pgvector, Embeddings, and Retrieval Pipelines

Build a production-ready retrieval-augmented generation pipeline in Laravel using pgvector, OpenAI embeddings,...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/practical-rag-in-laravel-pgvector-embeddings-and-retrieval-pipelines-2) [ ![Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State](https://cdn.msaied.com/486/7a50572cb8b282ae36063a9213f55ed3.png) laravel octane frankenphp 

### Laravel Octane + FrankenPHP: Persistent Services, Boot-Once Singletons, and Safe State

Running Laravel under FrankenPHP with Octane means your service container lives across requests. Learn which s...

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

 30 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-octane-frankenphp-persistent-services-boot-once-singletons-and-safe-state) 

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