Laravel Concurrency &amp; Job Batching for Parallel Work | 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 with Laravel Concurrency: Parallel Work Without the Chaos        On this page       1. [  The Problem: Sequential Work Masquerading as Async ](#the-problem-sequential-work-masquerading-as-async)
2. [  The Concurrency Facade in One Minute ](#the-concurrency-facade-in-one-minute)
3. [  Choosing the Right Driver ](#choosing-the-right-driver)
4. [  Combining Concurrency with Job Batching ](#combining-concurrency-with-job-batching)
5. [  Controlling Concurrency on the Batch Itself ](#controlling-concurrency-on-the-batch-itself)
6. [  Pitfalls to Avoid ](#pitfalls-to-avoid)
7. [  1. Passing Eloquent Models into Concurrency Closures ](#1-passing-eloquent-models-into-concurrency-closures)
8. [  2. Assuming Shared Cache State ](#2-assuming-shared-cache-state)
9. [  3. Ignoring the sync Driver in Tests ](#3-ignoring-the-codesynccode-driver-in-tests)
10. [  Takeaways ](#takeaways)

  ![Job Batching with Laravel Concurrency: Parallel Work Without the Chaos](https://cdn.msaied.com/559/d55e86a2ecbe32de5e2196f5be63511d.png)

  #laravel   #concurrency   #queues   #async  

 Job Batching with Laravel Concurrency: Parallel Work Without the Chaos 
========================================================================

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

       Table of contents

  10 sections  

1. [  01   The Problem: Sequential Work Masquerading as Async  ](#the-problem-sequential-work-masquerading-as-async)
2. [  02   The Concurrency Facade in One Minute  ](#the-concurrency-facade-in-one-minute)
3. [  03   Choosing the Right Driver  ](#choosing-the-right-driver)
4. [  04   Combining Concurrency with Job Batching  ](#combining-concurrency-with-job-batching)
5. [  05   Controlling Concurrency on the Batch Itself  ](#controlling-concurrency-on-the-batch-itself)
6. [  06   Pitfalls to Avoid  ](#pitfalls-to-avoid)
7. [  07   1. Passing Eloquent Models into Concurrency Closures  ](#1-passing-eloquent-models-into-concurrency-closures)
8. [  08   2. Assuming Shared Cache State  ](#2-assuming-shared-cache-state)
9. [  09   3. Ignoring the sync Driver in Tests  ](#3-ignoring-the-codesynccode-driver-in-tests)
10. [  10   Takeaways  ](#takeaways)

       The Problem: Sequential Work Masquerading as Async
--------------------------------------------------

Most Laravel applications that need to "do several things at once" end up dispatching jobs and hoping for the best. That works until you need the *results* of those jobs before moving on — think generating a multi-section report, enriching a batch of records from three external APIs, or running independent validation pipelines in parallel.

Laravel 11 shipped the `Concurrency` facade to solve exactly this. Combined with job batching, you can express parallel work clearly and handle failures without writing a custom orchestration layer.

---

The Concurrency Facade in One Minute
------------------------------------

`Concurrency::run()` accepts an array of closures and executes them in separate PHP processes (via the `fork` driver on Linux/macOS, or a queue-backed driver elsewhere). Each closure is isolated — no shared memory, no race conditions on your objects.

```php
use Illuminate\Support\Facades\Concurrency;

[$orders, $inventory, $pricing] = Concurrency::run([
    fn () => Order::whereUserId($userId)->get(),
    fn () => Inventory::forUser($userId)->available()->get(),
    fn () => PricingService::currentRates(),
]);

```

The return value is an array of results in the same order as the input. Exceptions bubble up as an `\Illuminate\Process\Exceptions\ProcessFailedException` — catch it or let it propagate.

### Choosing the Right Driver

| Driver | When to use | |---|---| | `fork` | CLI/queue workers on Linux; fastest, zero overhead | | `process` | Same as fork but spawns a full PHP process; safer for memory | | `sync` | Testing; runs closures sequentially, no forking |

Set the driver in `config/concurrency.php` or per-call:

```php
Concurrency::driver('fork')->run([...]);

```

---

Combining Concurrency with Job Batching
---------------------------------------

Concurrency is great for short-lived, CPU-bound or I/O-bound tasks that return values. Job batching is better for long-running, queue-distributed work where you need callbacks on completion or failure.

The sweet spot: use `Concurrency` to *fan out* fast preparatory work, then dispatch a `Bus::batch()` for the heavy lifting.

```php
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Concurrency;

// Step 1: fetch metadata in parallel (fast, returns values)
[$segments, $schema] = Concurrency::run([
    fn () => DataSegmentRepository::forExport($exportId),
    fn () => SchemaRegistry::resolve($exportId),
]);

// Step 2: dispatch a batch for the heavy per-segment work
$batch = Bus::batch(
    $segments->map(fn ($seg) => new ProcessSegmentJob($seg, $schema))
)->then(function (Batch $batch) use ($exportId) {
    Export::find($exportId)->markComplete();
})->catch(function (Batch $batch, Throwable $e) use ($exportId) {
    Export::find($exportId)->markFailed($e->getMessage());
})->allowFailures()
  ->dispatch();

```

### Controlling Concurrency on the Batch Itself

Batches don't limit concurrency by default — your queue workers do. If you need to throttle, attach the `WithoutOverlapping` middleware or a rate-limited middleware to the job:

```php
public function middleware(): array
{
    return [
        new RateLimited('segment-processing'),
    ];
}

public function retryUntil(): DateTime
{
    return now()->addMinutes(10);
}

```

---

Pitfalls to Avoid
-----------------

### 1. Passing Eloquent Models into Concurrency Closures

Closures are serialized before being forked. Eloquent models serialize fine, but their open database connections do not. Always pass IDs and re-query inside the closure:

```php
// Bad
Concurrency::run([fn () => $user->enrichProfile()]);

// Good
$userId = $user->id;
Concurrency::run([fn () => User::find($userId)->enrichProfile()]);

```

### 2. Assuming Shared Cache State

Forked processes inherit the parent's memory snapshot but not live cache writes made after the fork. Treat each closure as a fresh request.

### 3. Ignoring the `sync` Driver in Tests

The `sync` driver runs closures sequentially, which is exactly what you want in Pest tests. Bind it in `TestCase::setUp` or use `Concurrency::fake()`.

```php
Concurrency::fake([
    fn () => collect([/* stubbed segments */]),
    fn () => new SchemaStub(),
]);

```

---

Takeaways
---------

- Use `Concurrency::run()` for fast, value-returning parallel tasks; use `Bus::batch()` for distributed, long-running work.
- Always pass primitive IDs into concurrency closures — never live model instances or open connections.
- The `fork` driver is fastest on Linux workers; fall back to `process` if you hit memory issues.
- Test with `Concurrency::fake()` to keep your Pest suite deterministic.
- Combine batch `->then()` / `->catch()` callbacks with `->allowFailures()` for resilient pipelines that report partial success.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fjob-batching-with-laravel-concurrency-parallel-work-without-the-chaos&text=Job+Batching+with+Laravel+Concurrency%3A+Parallel+Work+Without+the+Chaos) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fjob-batching-with-laravel-concurrency-parallel-work-without-the-chaos) 

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

  3 questions  

     Q01  When should I use the Concurrency facade instead of dispatching jobs to a queue?        Use Concurrency::run() when you need the results immediately in the same request or CLI command and the work is short-lived (under a few seconds). Use queued jobs when the work is long-running, needs retries, or the caller does not need to wait for results. 

      Q02  Does the Concurrency facade work on shared hosting or Windows?        The fork driver requires pcntl, which is unavailable on Windows and most shared hosts. Switch to the process driver (spawns a full PHP subprocess) or the sync driver for environments without pcntl support. 

      Q03  How do I handle exceptions thrown inside a Concurrency::run() closure?        If any closure throws, Concurrency::run() re-throws the exception in the parent process after all closures finish. Wrap the call in a try/catch and inspect the exception; partial results from successful closures are not returned when an exception occurs. 

  Continue reading

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

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

 [ ![PostgreSQL Partial and Covering Indexes for Laravel Query Performance](https://cdn.msaied.com/558/40ed2a1b013ac76dc4d08f1a245c5747.png) postgresql laravel performance 

### PostgreSQL Partial and Covering Indexes for Laravel Query Performance

Learn how partial and covering indexes eliminate unnecessary index bloat and let PostgreSQL satisfy queries en...

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

 17 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/postgresql-partial-and-covering-indexes-for-laravel-query-performance) [ ![Filament at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning](https://cdn.msaied.com/557/7c7cc76acf702e58f5175e1308414ec8.png) filament laravel multi-tenant 

### Filament at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning

Running Filament across multiple panels with distinct auth guards and tuning table queries for large datasets...

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

 16 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-at-scale-multi-panel-auth-custom-panels-and-table-query-tuning-4) [ ![Contextual Macros and Mixins: Extending Laravel Collections Without Bloat](https://cdn.msaied.com/556/0c5a2892229d005cb3b747c868df5bb6.png) laravel collections macros 

### Contextual Macros and Mixins: Extending Laravel Collections Without Bloat

Learn how to add domain-specific behaviour to Laravel's Collection class using macros, mixins, and higher-orde...

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

 16 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/contextual-macros-and-mixins-extending-laravel-collections-without-bloat) 

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