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 dispatching: learn how Laravel's Bus::batch(), job chains, and rate-limited middleware compose into reliable, observable async workflows without reaching for a dedicated workflow engine.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 17 Jun 2026 · Updated 17 Jun 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

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

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

  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.

   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.

   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.

   ![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 Octane + FrankenPHP: Persistent State, Shared Services, and Safe Bootstrapping](https://www.msaied.com/public/articles/laravel-octane-frankenphp-persistent-state-shared-services-and-safe-bootstrapping) [Next articleLaravel Live Japan 2026 Recap: East Asia's First Official Laravel Conference](https://www.msaied.com/public/articles/laravel-live-japan-2026-recap-east-asias-first-official-laravel-conference)  

   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)

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