June 18, 2026
Concurrency, Parallelism & Async in Laravel 13 + Livewire 4 — How to Handle Multiple Tasks…
You’ve already optimized your queries, added eager loading, and slapped a cache layer on everything. Yet your app still drags at certain…

By Developer Awam
10 min read
You've already optimized your queries, added eager loading, and slapped a cache layer on everything. Yet your app still drags at certain points. Here's the thing: the bottleneck might not be your database at all — it might be the order in which you're running things.
Picture this: your dashboard needs to pull data from three different APIs, but it waits for each one to finish before calling the next. Or you have a report export that forces users to stare at a spinner for 10 seconds. These aren't "server is too slow" problems. They're execution strategy problems.
In this article, we'll break down the real difference between concurrency, parallelism, and async — and more importantly, how to actually use them in Laravel 13 and Livewire 4 without turning your codebase into a mess.
We've Been Doing Things One at a Time
Think about making breakfast. You've got three things to handle: boiling water, toasting bread, and frying an egg. If you wait for the water to fully boil before you even touch the toaster — that's the most inefficient morning routine imaginable.
But that's exactly how PHP and Laravel work by default: synchronous and single-threaded. One request, one thread, one task at a time, in sequence.
Before we dive into solutions, let's get these three terms straight:
- Concurrency — You have multiple tasks and you're juggling them rapidly, switching back and forth so fast it feels like they're running at the same time. Think of a chef moving between multiple pans on the stove.
- Parallelism — Tasks are literally running at the same time, on separate processes or CPU cores. Two chefs, two stoves, cooking simultaneously.
- Async — You kick off a task, don't wait around for it to finish, and move on. You'll collect the result when it's ready.
Every parallel system is concurrent, but not every concurrent system is parallel.
Laravel runs on PHP-FPM, which is synchronous and single-threaded by default. But Laravel gives you plenty of ways to achieve concurrent and even parallel behavior — no need to jump ship to Go or Node.js.
Let's go through each tool.
1. Queues & Jobs — The Classic Concurrency Workhorse
This is the most battle-tested approach. When you have a heavy task and the user doesn't need the result immediately — push it to a queue.
When to use it
- Sending a welcome email after registration
- Generating a PDF report in the background
- Processing a CSV file with thousands of rows
- Sending push notifications to a large user base
Creating a Job
php artisan make:job ProcessOrderReport
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrderReport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public int $orderId) {}
public function handle(): void
{
// Heavy lifting happens here - generate PDF, send email, etc.
// The user already has their response. This runs in the background.
\Log::info("Processing order report: {$this->orderId}");
}
}php artisan make:job ProcessOrderReport
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrderReport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public int $orderId) {}
public function handle(): void
{
// Heavy lifting happens here - generate PDF, send email, etc.
// The user already has their response. This runs in the background.
\Log::info("Processing order report: {$this->orderId}");
}
}Dispatching from a controller
// Dispatch immediately
ProcessOrderReport::dispatch($order->id);
// Or delay it by 5 minutes
ProcessOrderReport::dispatch($order->id)->delay(now()->addMinutes(5));// Dispatch immediately
ProcessOrderReport::dispatch($order->id);
// Or delay it by 5 minutes
ProcessOrderReport::dispatch($order->id)->delay(now()->addMinutes(5));Job Batching — Many tasks, one command
One of the underrated features in Laravel is Job Batching. You can dispatch a group of jobs and hook into lifecycle events when they finish:
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$batch = Bus::batch([
new ProcessOrderReport(1),
new ProcessOrderReport(2),
new ProcessOrderReport(3),
])->then(function (Batch $batch) {
// All jobs finished - notify the admin
\Log::info('All reports have been processed!');
})->catch(function (Batch $batch, \Throwable $e) {
// Something went wrong
\Log::error('A job failed: ' . $e->getMessage());
})->dispatch();use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$batch = Bus::batch([
new ProcessOrderReport(1),
new ProcessOrderReport(2),
new ProcessOrderReport(3),
])->then(function (Batch $batch) {
// All jobs finished - notify the admin
\Log::info('All reports have been processed!');
})->catch(function (Batch $batch, \Throwable $e) {
// Something went wrong
\Log::error('A job failed: ' . $e->getMessage());
})->dispatch();Workers process these jobs concurrently — the more workers you spin up, the more parallel your processing becomes.
Running multiple workers
# Start 3 workers at the same time (separate terminal tabs)
php artisan queue:work --queue=default
php artisan queue:work --queue=default
php artisan queue:work --queue=default# Start 3 workers at the same time (separate terminal tabs)
php artisan queue:work --queue=default
php artisan queue:work --queue=default
php artisan queue:work --queue=defaultIn production, use Supervisor to manage workers automatically. More workers = more jobs processed in parallel at any given moment.
2. The Concurrency Facade — Built-in Parallel Processing in Laravel 13
This one flies under the radar more than it should. Laravel 13 ships with a Concurrency facade that lets you run multiple closures simultaneously in separate child PHP processes.
Under the hood, Laravel serializes your closures, dispatches them to hidden Artisan CLI processes, and then collects the results once all of them are done.
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
[$userCount, $orderCount, $productCount] = Concurrency::run([
fn () => DB::table('users')->count(),
fn () => DB::table('orders')->count(),
fn () => DB::table('products')->count(),
]);
// All three queries run at the same time
// Not: query 1 → wait → query 2 → wait → query 3use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
[$userCount, $orderCount, $productCount] = Concurrency::run([
fn () => DB::table('users')->count(),
fn () => DB::table('orders')->count(),
fn () => DB::table('products')->count(),
]);
// All three queries run at the same time
// Not: query 1 → wait → query 2 → wait → query 3If you prefer named results over positional ones, use an associative array:
$results = Concurrency::run([
'users' => fn () => DB::table('users')->count(),
'orders' => fn () => DB::table('orders')->count(),
'products' => fn () => DB::table('products')->count(),
]);
echo $results['users'];
echo $results['orders'];$results = Concurrency::run([
'users' => fn () => DB::table('users')->count(),
'orders' => fn () => DB::table('orders')->count(),
'products' => fn () => DB::table('products')->count(),
]);
echo $results['users'];
echo $results['orders'];Available drivers
Laravel 13 gives you three drivers for the Concurrency facade:
- process (default) — Spawns a new child process. Works in both web requests and CLI contexts.
- fork — Faster than
process, but CLI-only (Artisan commands, queue workers). Cannot be used during a web request. - sync — No concurrency at all. Runs closures in sequence. Perfect for testing.
To use the fork driver (in CLI / queue contexts):
composer require spatie/fork
$results = Concurrency::driver('fork')->run([
fn () => heavyCalculation(),
fn () => anotherHeavyTask(),
]);composer require spatie/fork
$results = Concurrency::driver('fork')->run([
fn () => heavyCalculation(),
fn () => anotherHeavyTask(),
]);Setting a timeout
You can cap how long each task is allowed to run so nothing hangs indefinitely:
use Illuminate\Support\Facades\Concurrency;
[$result1, $result2] = Concurrency::run([
fn () => callExternalApi(),
fn () => processLocalData(),
], timeout: 30); // max 30 seconds per taskuse Illuminate\Support\Facades\Concurrency;
[$result1, $result2] = Concurrency::run([
fn () => callExternalApi(),
fn () => processLocalData(),
], timeout: 30); // max 30 seconds per taskConcurrency facade vs. Queues — which one?
- Use the Concurrency facade when you need the results right now and each task takes a matter of seconds.
- Use Queues when tasks run for a long time, the result isn't needed immediately, or you need retry logic.
3. Async HTTP with Http::pool() — Hit Multiple APIs at Once
This might be the most impactful, least-known feature in this whole article. Suppose your dashboard page needs data from three different external endpoints. If you call them sequentially:
Request to API 1 → 800ms
Request to API 2 → 600ms
Request to API 3 → 700ms
Total: ~2100ms 😬Request to API 1 → 800ms
Request to API 2 → 600ms
Request to API 3 → 700ms
Total: ~2100ms 😬With Http::pool(), all three requests fire at the same time:
All requests run simultaneously
Total: ~800ms (only as slow as the slowest request) 😎All requests run simultaneously
Total: ~800ms (only as slow as the slowest request) 😎How to use it
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('weather')->get('https://api.weather.com/current', ['city' => 'London']),
$pool->as('stocks') ->get('https://api.stocks.com/prices', ['symbols' => 'AAPL,GOOG']),
$pool->as('news') ->get('https://api.news.com/latest', ['category' => 'tech']),
]);
// Access each response by name
if ($responses['weather']->ok()) {
$weatherData = $responses['weather']->json();
}
if ($responses['stocks']->ok()) {
$stockData = $responses['stocks']->json();
}use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('weather')->get('https://api.weather.com/current', ['city' => 'London']),
$pool->as('stocks') ->get('https://api.stocks.com/prices', ['symbols' => 'AAPL,GOOG']),
$pool->as('news') ->get('https://api.news.com/latest', ['category' => 'tech']),
]);
// Access each response by name
if ($responses['weather']->ok()) {
$weatherData = $responses['weather']->json();
}
if ($responses['stocks']->ok()) {
$stockData = $responses['stocks']->json();
}Cap the concurrency
If you're firing a large batch of requests and don't want to hammer the target server all at once:
$responses = Http::pool(
fn (Pool $pool) => collect($userIds)->map(
fn ($id) => $pool->as("user_{$id}")->get("https://api.example.com/user/{$id}")
)->all(),
concurrency: 5 // max 5 requests in-flight at any given time
);$responses = Http::pool(
fn (Pool $pool) => collect($userIds)->map(
fn ($id) => $pool->as("user_{$id}")->get("https://api.example.com/user/{$id}")
)->all(),
concurrency: 5 // max 5 requests in-flight at any given time
);One gotcha worth knowing
The pool() method can't be chained with withHeaders() or middleware at the pool level. If you need custom headers, set them on each individual request inside the pool:
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('data1')
->withToken('Bearer ' . $apiKey)
->get('https://api.example.com/data/1'),
$pool->as('data2')
->withToken('Bearer ' . $apiKey)
->get('https://api.example.com/data/2'),
]);$responses = Http::pool(fn (Pool $pool) => [
$pool->as('data1')
->withToken('Bearer ' . $apiKey)
->get('https://api.example.com/data/1'),
$pool->as('data2')
->withToken('Bearer ' . $apiKey)
->get('https://api.example.com/data/2'),
]);4. Livewire 4 — Smarter Lazy Loading and Polling
Now let's shift to the frontend side of things — or rather, the full-stack side, since this is Livewire. Version 4 landed at Laracon US 2025, and it brought meaningful performance improvements that are directly relevant to everything we've been talking about.
Lazy Loading — Stop Blocking the Initial Page Load
A classic problem: you have a dashboard with several heavy components. If all of them load at the same time, the whole page is held hostage by the slowest one.
Livewire 4 fixes this cleanly with lazy loading.
// resources/views/components/⚡revenue-chart.blade.php
<?php
use Livewire\Component;
use App\Models\Order;
new class extends Component {
public $data;
public function mount(): void
{
// This heavy query won't run on the initial page load
// It only runs when this component is lazy-loaded
$this->data = Order::query()
->selectRaw('DATE(created_at) as date, SUM(total) as revenue')
->groupBy('date')
->get();
}
public function render()
{
return view('livewire.revenue-chart');
}
}
?>// resources/views/components/⚡revenue-chart.blade.php
<?php
use Livewire\Component;
use App\Models\Order;
new class extends Component {
public $data;
public function mount(): void
{
// This heavy query won't run on the initial page load
// It only runs when this component is lazy-loaded
$this->data = Order::query()
->selectRaw('DATE(created_at) as date, SUM(total) as revenue')
->groupBy('date')
->get();
}
public function render()
{
return view('livewire.revenue-chart');
}
}
?>In your dashboard or page layout:
<div class="grid grid-cols-2 gap-4">
{{-- Fast component — loads immediately --}}
<livewire:quick-stats />
{{-- Slow component - lazy loads when scrolled into viewport --}}
<livewire:revenue-chart lazy />
{{-- Defer - loads right after the initial page render completes --}}
<livewire:monthly-summary defer />
</div><div class="grid grid-cols-2 gap-4">
{{-- Fast component — loads immediately --}}
<livewire:quick-stats />
{{-- Slow component - lazy loads when scrolled into viewport --}}
<livewire:revenue-chart lazy />
{{-- Defer - loads right after the initial page render completes --}}
<livewire:monthly-summary defer />
</div>A key detail here: in Livewire 4, lazy and deferred requests are isolated from each other by default — meaning they load in parallel, not one after another. This happens automatically, no extra config needed.
Islands in Livewire 4 — Isolate the Slow Parts
Livewire 4 also introduces Islands — a way to carve out specific sections of a page so slow parts don't block fast ones.
<div class="dashboard">
{{-- Fast section — renders immediately --}}
<x-metric-grid :metrics="$quickMetrics" />
{{-- Slow section - isolated and lazy loaded --}}
@island('revenue', lazy: true)
@placeholder
<x-revenue-skeleton />
@endplaceholder
<x-revenue-chart :data="$expensiveRevenueData" />
@endisland
{{-- Auto-polling island - stays fresh without affecting the rest of the page --}}
@island('live-orders', poll: '5s')
<x-live-order-feed />
@endisland
</div><div class="dashboard">
{{-- Fast section — renders immediately --}}
<x-metric-grid :metrics="$quickMetrics" />
{{-- Slow section - isolated and lazy loaded --}}
@island('revenue', lazy: true)
@placeholder
<x-revenue-skeleton />
@endplaceholder
<x-revenue-chart :data="$expensiveRevenueData" />
@endisland
{{-- Auto-polling island - stays fresh without affecting the rest of the page --}}
@island('live-orders', poll: '5s')
<x-live-order-feed />
@endisland
</div>Placeholder — Give Users Something to Look At
While a component is lazily loading, you can show a skeleton or spinner so users don't feel like nothing is happening:
<div>
@placeholder
{{-- Shown while the component loads --}}
<div class="space-y-3 animate-pulse">
<div class="h-4 bg-gray-300 rounded w-3/4"></div>
<div class="h-4 bg-gray-300 rounded w-1/2"></div>
<div class="h-4 bg-gray-300 rounded w-5/6"></div>
</div>
@endplaceholder
{{-- Real content appears after load --}}
<div class="chart-container">
{{-- chart content --}}
</div>
</div><div>
@placeholder
{{-- Shown while the component loads --}}
<div class="space-y-3 animate-pulse">
<div class="h-4 bg-gray-300 rounded w-3/4"></div>
<div class="h-4 bg-gray-300 rounded w-1/2"></div>
<div class="h-4 bg-gray-300 rounded w-5/6"></div>
</div>
@endplaceholder
{{-- Real content appears after load --}}
<div class="chart-container">
{{-- chart content --}}
</div>
</div>wire:poll — Real-Time Updates Without a WebSocket
When you need data that keeps refreshing — order status, notification count, live metrics — wire:poll is the quickest way to get there without setting up WebSockets or Laravel Echo.
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Order;
class OrderStatusMonitor extends Component
{
public int $orderId;
public string $status = 'pending';
public ?string $error = null;
public function refreshStatus(): void
{
try {
$this->status = Order::find($this->orderId)?->status ?? 'unknown';
$this->error = null;
} catch (\Exception $e) {
$this->error = 'Failed to fetch the latest status.';
}
}
public function render()
{
return view('livewire.order-status-monitor');
}
}
{{-- Poll every 5 seconds, but only when visible in the viewport --}}
<div wire:poll.5000ms.visible="refreshStatus">
@if($error)
<p class="text-red-500 text-sm">{{ $error }}</p>
@else
<span class="badge badge-{{ $status }}">{{ ucfirst($status) }}</span>
@endif
</div><?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Order;
class OrderStatusMonitor extends Component
{
public int $orderId;
public string $status = 'pending';
public ?string $error = null;
public function refreshStatus(): void
{
try {
$this->status = Order::find($this->orderId)?->status ?? 'unknown';
$this->error = null;
} catch (\Exception $e) {
$this->error = 'Failed to fetch the latest status.';
}
}
public function render()
{
return view('livewire.order-status-monitor');
}
}
{{-- Poll every 5 seconds, but only when visible in the viewport --}}
<div wire:poll.5000ms.visible="refreshStatus">
@if($error)
<p class="text-red-500 text-sm">{{ $error }}</p>
@else
<span class="badge badge-{{ $status }}">{{ ucfirst($status) }}</span>
@endif
</div>What Livewire 4 does automatically for you
There are a few built-in optimizations worth knowing:
- Background tab throttling — When a page is in an inactive browser tab, Livewire automatically cuts polling requests by 95%. So if a user has 10 tabs open, your server isn't getting hammered 10x.
.visiblemodifier — Polling only fires when the element is actually visible in the viewport. Great for components further down the page..keep-alivemodifier — If you genuinely need polling to continue even in a background tab, use this.- Non-blocking polls — In Livewire 4, background poll requests no longer block user-initiated actions. If a user clicks a button while a poll is in-flight, their action gets priority.
{{-- Only poll when visible in viewport --}}
<div wire:poll.visible="$refresh">...</div>
{{-- Keep polling even in background tabs --}}
<div wire:poll.keep-alive="checkStatus">...</div>
{{-- Poll every 10 seconds, visible only --}}
<div wire:poll.10000ms.visible="refreshData">...</div>{{-- Only poll when visible in viewport --}}
<div wire:poll.visible="$refresh">...</div>
{{-- Keep polling even in background tabs --}}
<div wire:poll.keep-alive="checkStatus">...</div>
{{-- Poll every 10 seconds, visible only --}}
<div wire:poll.10000ms.visible="refreshData">...</div>Events over polling — the smarter alternative
If you know when data changes (for example, after a background job finishes), events are far more efficient than polling. You only make a request when there's actually something new to show.
// Inside the background job
class ProcessOrderReport implements ShouldQueue
{
public function handle(): void
{
// ... process the report ...
// Signal that the report is ready
// (could also use Laravel Broadcasting + Echo for websockets)
cache()->put("report_ready_{$this->orderId}", true, 60);
}
}
// In your Livewire component — check the flag, not blind polling
public function checkIfReady(): void
{
if (cache()->has("report_ready_{$this->orderId}")) {
$this->reportReady = true;
$this->dispatch('report-generated');
}
}// Inside the background job
class ProcessOrderReport implements ShouldQueue
{
public function handle(): void
{
// ... process the report ...
// Signal that the report is ready
// (could also use Laravel Broadcasting + Echo for websockets)
cache()->put("report_ready_{$this->orderId}", true, 60);
}
}
// In your Livewire component — check the flag, not blind polling
public function checkIfReady(): void
{
if (cache()->has("report_ready_{$this->orderId}")) {
$this->reportReady = true;
$this->dispatch('report-generated');
}
}Putting It All Together — A Real-World Dashboard
Let's walk through a concrete example: an Admin Dashboard that pulls data from multiple sources.
The inefficient approach (before)
// Controller — everything runs sequentially, one after another
public function dashboard()
{
$userStats = $this->userService->getStats(); // ~400ms
$orderStats = $this->orderService->getStats(); // ~350ms
$revenueData = $this->revenueService->getMonthly(); // ~600ms
$apiData = Http::get('https://external-api.com/rates'); // ~800ms
// Total: ~2150ms before the page can even start rendering
return view('dashboard', compact('userStats', 'orderStats', 'revenueData', 'apiData'));
}// Controller — everything runs sequentially, one after another
public function dashboard()
{
$userStats = $this->userService->getStats(); // ~400ms
$orderStats = $this->orderService->getStats(); // ~350ms
$revenueData = $this->revenueService->getMonthly(); // ~600ms
$apiData = Http::get('https://external-api.com/rates'); // ~800ms
// Total: ~2150ms before the page can even start rendering
return view('dashboard', compact('userStats', 'orderStats', 'revenueData', 'apiData'));
}The efficient approach (after)
Step 1 — Data that doesn't depend on each other and needs to be returned to the view → Concurrency::run() or Http::pool().
Step 2 — Heavy components on the page → Livewire 4 lazy loading.
Step 3 — Data that needs real-time updates → wire:poll with the right modifiers.
// Service layer — run independent tasks in parallel
use Illuminate\Support\Facades\Concurrency;
public function getDashboardData(): array
{
// Total time = slowest task, not the sum of all tasks
[$userStats, $orderStats, $revenueData] = Concurrency::run([
fn () => $this->userService->getStats(),
fn () => $this->orderService->getStats(),
fn () => $this->revenueService->getMonthly(),
], timeout: 10);
return compact('userStats', 'orderStats', 'revenueData');
}
// For external APIs — fire them all at once
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
$externalData = Http::pool(fn (Pool $pool) => [
$pool->as('rates') ->get('https://api.currency.com/rates'),
$pool->as('weather') ->get('https://api.weather.com/london'),
$pool->as('analytics')->get('https://api.analytics.com/summary'),
]);
{{-- Dashboard page in Livewire 4 --}}
<div class="grid grid-cols-3 gap-6">
{{-- Fast stats - load right away --}}
<livewire:user-stats />
<livewire:order-stats />
{{-- Revenue chart - heavy query, lazy load on scroll --}}
<livewire:revenue-chart lazy />
{{-- Live order feed - poll every 8s, only when visible --}}
<div wire:poll.8000ms.visible="$refresh">
<livewire:live-orders />
</div>
</div>// Service layer — run independent tasks in parallel
use Illuminate\Support\Facades\Concurrency;
public function getDashboardData(): array
{
// Total time = slowest task, not the sum of all tasks
[$userStats, $orderStats, $revenueData] = Concurrency::run([
fn () => $this->userService->getStats(),
fn () => $this->orderService->getStats(),
fn () => $this->revenueService->getMonthly(),
], timeout: 10);
return compact('userStats', 'orderStats', 'revenueData');
}
// For external APIs — fire them all at once
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
$externalData = Http::pool(fn (Pool $pool) => [
$pool->as('rates') ->get('https://api.currency.com/rates'),
$pool->as('weather') ->get('https://api.weather.com/london'),
$pool->as('analytics')->get('https://api.analytics.com/summary'),
]);
{{-- Dashboard page in Livewire 4 --}}
<div class="grid grid-cols-3 gap-6">
{{-- Fast stats - load right away --}}
<livewire:user-stats />
<livewire:order-stats />
{{-- Revenue chart - heavy query, lazy load on scroll --}}
<livewire:revenue-chart lazy />
{{-- Live order feed - poll every 8s, only when visible --}}
<div wire:poll.8000ms.visible="$refresh">
<livewire:live-orders />
</div>
</div>Quick Reference — Which Tool for Which Job
Before you reach for the queue every time something feels slow, here's a quick mental map:
- Queue & Jobs — Use when the task is heavy, the user doesn't need the result immediately, and you want retry logic baked in. Good for: sending emails, generating PDFs, processing large file uploads.
- Concurrency Facade — Use when you need results right now from multiple independent tasks. Great for aggregating data from several sources in a single request.
- Http::pool() — Specifically for firing multiple HTTP requests to different endpoints simultaneously. A huge win on integration-heavy dashboards.
- Livewire Lazy Loading — Use for components with expensive queries or processing that can be deferred until after the initial page render. Users get immediate visual feedback instead of staring at a blank screen.
- wire:poll — For data that needs periodic refreshing. Add
.visiblealmost by default, and consider switching to event-based updates if polling starts feeling excessive.
Closing Thoughts
Concurrency and parallelism in Laravel aren't things you need to build from scratch. The ecosystem already has the tools — mature Queue workers, the newer Concurrency facade, Http pool for async HTTP calls, and Livewire 4's automatically parallel lazy loading.
The real skill is picking the right tool for the right situation. Not every slow thing needs a queue. Not every real-time feature needs WebSockets.
Audit your app: where are the actual bottlenecks? If you find things being done in sequence that could run in parallel — that's exactly where these tools shine.
Happy shipping.