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 Work in Laravel Queues ](#beyond-codedispatchcode-composing-work-in-laravel-queues)
2. [  Job Batching with Bus::batch() ](#job-batching-with-codebusbatchcode)
3. [  Key batch options ](#key-batch-options)
4. [  Safe Job Chaining ](#safe-job-chaining)
5. [  Passing state between chained jobs ](#passing-state-between-chained-jobs)
6. [  Rate-Limited Middleware ](#rate-limited-middleware)
7. [  Combining with WithoutOverlapping ](#combining-with-codewithoutoverlappingcode)
8. [  Mixing Batches and Chains ](#mixing-batches-and-chains)
9. [  Takeaways ](#takeaways)

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

  #laravel   #queues   #jobs   #concurrency  

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

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

       Table of contents

  9 sections  

1. [  01   Beyond dispatch(): Composing Work in Laravel Queues  ](#beyond-codedispatchcode-composing-work-in-laravel-queues)
2. [  02   Job Batching with Bus::batch()  ](#job-batching-with-codebusbatchcode)
3. [  03   Key batch options  ](#key-batch-options)
4. [  04   Safe Job Chaining  ](#safe-job-chaining)
5. [  05   Passing state between chained jobs  ](#passing-state-between-chained-jobs)
6. [  06   Rate-Limited Middleware  ](#rate-limited-middleware)
7. [  07   Combining with WithoutOverlapping  ](#combining-with-codewithoutoverlappingcode)
8. [  08   Mixing Batches and Chains  ](#mixing-batches-and-chains)
9. [  09   Takeaways  ](#takeaways)

       Beyond `dispatch()`: Composing Work in Laravel Queues
-----------------------------------------------------

Most Laravel applications outgrow simple fire-and-forget dispatches quickly. When you need to process a CSV of 50,000 rows, sync data to three external APIs, then send a summary email — you need batching, chaining, and throttling working together.

---

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

Batches let you dispatch a collection of jobs and react to their collective outcome.

```php
use Illuminate\Support\Facades\Bus;
use App\Jobs\ProcessOrderRow;

$batch = Bus::batch(
    $orders->map(fn ($order) => new ProcessOrderRow($order))
)->then(function (\Illuminate\Bus\Batch $batch) {
    // All jobs succeeded
    SummaryNotification::dispatch($batch->id);
})->catch(function (\Illuminate\Bus\Batch $batch, \Throwable $e) {
    // First failure — batch continues unless you call $batch->cancel()
    Log::error('Batch failure', ['batch' => $batch->id, 'error' => $e->getMessage()]);
})->finally(function (\Illuminate\Bus\Batch $batch) {
    // Always runs — success or failure
    BatchAudit::record($batch->id, $batch->failedJobs);
})->allowFailures()->dispatch();

```

### Key batch options

- `->allowFailures()` — the batch continues even when individual jobs fail; omit it to cancel on first failure.
- `->onQueue('imports')` — route the entire batch to a specific queue.
- `->name('order-import')` — label it for Horizon's UI.

Store the `$batch->id` on your model so you can poll progress via `Bus::findBatch($id)`.

---

Safe Job Chaining
-----------------

Chaining is sequential: each job only runs if the previous one succeeded.

```php
use App\Jobs\{FetchRemoteData, TransformData, PersistData};

FetchRemoteData::withChain([
    new TransformData(),
    new PersistData(),
])->dispatch($importId);

```

### Passing state between chained jobs

Chained jobs share no memory. Use a shared model or cache key:

```php
class FetchRemoteData implements ShouldQueue
{
    public function handle(): void
    {
        $raw = Http::get($this->url)->json();
        Cache::put("import:{$this->importId}:raw", $raw, now()->addHour());
    }
}

class TransformData implements ShouldQueue
{
    public function handle(): void
    {
        $raw = Cache::get("import:{$this->importId}:raw");
        // transform and store transformed result
    }
}

```

Avoid injecting large payloads into the job constructor — the serialised payload is stored in your queue backend.

---

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

When you're calling a third-party API with a 100 req/min cap, you need to throttle at the job level, not the HTTP client level.

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

// AppServiceProvider::boot()
RateLimiter::for('stripe-api', function () {
    return Limit::perMinute(80); // leave headroom
});

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

```

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

### Combining with `WithoutOverlapping`

For jobs that must not run concurrently per-tenant:

```php
use Illuminate\Queue\Middleware\{RateLimited, WithoutOverlapping};

public function middleware(): array
{
    return [
        new WithoutOverlapping($this->tenantId),
        new RateLimited('stripe-api'),
    ];
}

```

`WithoutOverlapping` uses an atomic cache lock; set `->expireAfter(120)` to avoid dead locks if a worker crashes mid-job.

---

Mixing Batches and Chains
-------------------------

You can nest chains inside a batch — each array element can itself be a chain:

```php
Bus::batch([
    [new ValidateRow($row1), new ImportRow($row1)],
    [new ValidateRow($row2), new ImportRow($row2)],
])->allowFailures()->dispatch();

```

Each inner array is treated as an ordered chain; the batch tracks them as a unit.

---

Takeaways
---------

- Use `->allowFailures()` on batches when partial success is acceptable; omit it when atomicity matters.
- Never pass large objects in job constructors — store state in cache or DB and reference by ID.
- `RateLimited` middleware releases jobs back to the queue automatically; pair it with a generous `tries` value.
- `WithoutOverlapping` prevents concurrent execution per key — always set `expireAfter` to handle worker crashes.
- Nest chains inside `Bus::batch()` arrays for parallel-but-ordered workflows.
- Monitor batch progress via `Bus::findBatch($id)->progress()` for real-time UI feedback.

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

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

  3 questions  

     Q01  What happens to a batch when one job fails and `allowFailures()` is not set?        Without `allowFailures()`, the batch is cancelled on the first failure. Pending jobs are not dispatched, but already-running jobs complete. The `catch` callback fires, and `finally` runs after everything settles. 

      Q02  How many times will a rate-limited job retry before it is marked failed?        A job released by `RateLimited` middleware does not consume a retry attempt by default. It keeps releasing until the rate limit window clears. Set `$tries` and `$maxExceptions` on the job to cap total attempts if you also want a hard failure ceiling. 

      Q03  Can I add more jobs to an existing batch after it has been dispatched?        Yes. Call `Bus::findBatch($id)-&gt;add([new AnotherJob()])` from within a job that belongs to the same batch. This is useful for dynamically discovered work, such as paginated API responses. 

  Continue reading

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

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

 [ ![Cursor Pagination, Chunked Iteration, and Lazy Collections at Scale in Laravel](https://cdn.msaied.com/355/3a6df23a2c16b740843260134fad7c63.png) laravel eloquent performance 

### Cursor Pagination, Chunked Iteration, and Lazy Collections at Scale in Laravel

Offset pagination breaks under large datasets. Learn how cursor pagination, chunked iteration, and lazy collec...

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

 3 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/cursor-pagination-chunked-iteration-and-lazy-collections-at-scale-in-laravel-1) [ ![Laravel Reverb: Building Presence Channels with Per-User State and Typed Events](https://cdn.msaied.com/352/9b3c490b8303fdc84442671965a3ee8a.png) laravel reverb websockets 

### Laravel Reverb: Building Presence Channels with Per-User State and Typed Events

Presence channels in Laravel Reverb go far beyond simple pub/sub. Learn how to track per-user state, broadcast...

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

 3 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-reverb-building-presence-channels-with-per-user-state-and-typed-events) [ ![Laravel Caching Strategies: Tags, Stampede Prevention, and Cache-Aside at Scale](https://cdn.msaied.com/351/ac186f395c8b0f7ae2e8ff230c61e323.png) laravel caching performance 

### Laravel Caching Strategies: Tags, Stampede Prevention, and Cache-Aside at Scale

Beyond simple remember() calls: how to use cache tags for granular invalidation, prevent cache stampedes with...

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

 3 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-caching-strategies-tags-stampede-prevention-and-cache-aside-at-scale-1) 

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