Why Concurrency Matters Before You Reach for a Queue
Queues are the right tool for deferred, durable work. But sometimes you need to fan out several independent HTTP calls, database reads, or CPU-bound transforms right now, within a single request, and collect the results before responding. Laravel's Concurrency facade (introduced in Laravel 11) and the underlying Process facade give you two clean primitives for this.
The Concurrency Facade
The Concurrency facade runs an array of closures in parallel using forked PHP processes under the hood. Each closure is serialized, executed in a child process, and the results are collected back into the parent.
use Illuminate\Support\Facades\Concurrency;
[$prices, $inventory, $reviews] = Concurrency::run([
fn () => PricingService::fetch(productId: 42),
fn () => InventoryService::fetch(productId: 42),
fn () => ReviewService::summary(productId: 42),
]);
All three calls happen in parallel. The parent blocks until every child finishes, then returns results in the same order as the input array.
What Gets Serialized
Because each closure runs in a forked process, anything captured in the closure must be serializable. Eloquent models, DTOs, and scalar values are fine. Database connections, open file handles, and non-serializable objects are not — they will cause silent failures or exceptions.
// ✅ Safe — scalar captured
$id = $product->id;
Concurrency::run([fn () => SomeService::fetch($id)]);
// ❌ Unsafe — Eloquent model captured directly
Concurrency::run([fn () => SomeService::fetch($product)]);
Process Pools for Lower-Level Control
When you need more control — custom environment variables, timeouts, or shell commands — the Process facade's pool API is the right layer.
use Illuminate\Support\Facades\Process;
$results = Process::pool(function ($pool) {
$pool->command('php artisan report:generate --type=sales');
$pool->command('php artisan report:generate --type=inventory');
$pool->command('php artisan report:generate --type=returns');
})->start()->wait();
foreach ($results as $result) {
if ($result->failed()) {
logger()->error($result->errorOutput());
}
}
Each command runs as a real OS process. start() launches them all immediately; wait() blocks until every process exits and returns a ProcessPoolResults collection.
Timeouts and Error Handling
Process::pool(function ($pool) {
$pool->timeout(30)->command('php artisan export:csv --month=2025-05');
$pool->timeout(10)->command('php artisan notify:slack');
})->start()->wait();
A timed-out process throws ProcessTimedOutException on wait(). Wrap the call in a try/catch and decide whether to retry or degrade gracefully.
PHP Fibers: Cooperative Concurrency Without Forking
Fibers (PHP 8.1+) are cooperative, not preemptive — they do not give you true parallelism. They are useful for interleaving I/O-bound work within a single thread, which is exactly how async libraries like ReactPHP and Revolt use them. Laravel itself uses Fibers internally in some Octane contexts.
For most Laravel applications, reach for Concurrency::run() or Process::pool() before writing raw Fiber code. Fibers shine when you control the event loop; in a standard FPM request, forked processes are simpler and safer.
Production Considerations
- Worker limits: Each forked process inherits the parent's memory footprint. On a 512 MB container, running 10 concurrent forks can exhaust memory quickly. Benchmark your payload size.
- Database connections: Child processes do not inherit open PDO connections safely. Let each child open its own connection via the service container.
- Octane compatibility: Under Octane, forked processes can inherit shared state. Prefer
Process::pool()with artisan commands when running under Octane to avoid state leakage. - Observability: Exceptions in child processes surface as serialized
Throwableinstances. Log them explicitly — they will not bubble up to your default exception handler automatically.
Key Takeaways
- Use
Concurrency::run()for parallel PHP closures when results are needed immediately in the same request. - Capture only serializable values in closures — never raw Eloquent models or connections.
- Use
Process::pool()when you need OS-level process control, timeouts, or shell commands. - PHP Fibers are cooperative, not parallel — they are not a replacement for process-based concurrency in FPM.
- Always account for memory multiplication and connection limits when sizing concurrent workloads.