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 dispatch: learn how to compose Laravel job batches with callbacks, chain dependent jobs safely, 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 3 Jul 2026 · Updated 3 Jul 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [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)
- [concurrency](https://www.msaied.com/public/articles?search=concurrency)

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

  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.

   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.

   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.

   ![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 articleLaravel Reverb: Building Presence Channels with Per-User State and Typed Events](https://www.msaied.com/public/articles/laravel-reverb-building-presence-channels-with-per-user-state-and-typed-events) [Next articleCursor Pagination, Chunked Iteration, and Lazy Collections at Scale in Laravel](https://www.msaied.com/public/articles/cursor-pagination-chunked-iteration-and-lazy-collections-at-scale-in-laravel-1)  

   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)

 ###  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)
