Queue Job Batching & Idempotency in Laravel
"Production-grade background processing in Laravel: job batching, atomic locks, and idempotent webhook handling."
Queue Job Batching & Idempotency in Laravel
Background queues form the backbone of high-throughput web applications. Two critical design pillars for production reliability are Job Batching and Job Idempotency.
1. Job Batching (Bus::batch)
Job batching allows dispatching a group of jobs with completion tracking, cancellation, and failure callbacks.
Database Setup
Ensure the batch table exists:
php artisan queue:batches-table
php artisan migrate
Job Implementation
Add the Batchable trait to the job:
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessUserReport implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public int $userId) {}
public function handle(): void
{
if ($this->batch()?->cancelled()) {
return;
}
// Process report logic...
}
}
Dispatching the Batch
use App\Jobs\ProcessUserReport;
use Illuminate\Support\Facades\Bus;
use Throwable;
$jobs = User::pluck('id')->map(fn ($id) => new ProcessUserReport($id));
$batch = Bus::batch($jobs)
->then(function () {
Log::info('All user reports generated successfully.');
})
->catch(function (Throwable $e) {
Log::error('Batch processing failed: ' . $e->getMessage());
})
->name('Export User Reports')
->dispatch();
2. Idempotency: Handling Retries and Webhook Duplication
Because distributed message queues operate on at-least-once delivery, jobs and payment webhooks can be delivered more than once. An idempotent job ensures that multiple executions produce the same state as a single execution.
Atomic Lock Pattern for Duplicate Prevention
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ProcessPaymentWebhook implements ShouldQueue
{
use Queueable;
public function __construct(
public string $eventReference,
public array $payload
) {}
public function handle(): void
{
$lock = Cache::lock('webhook:' . $this->eventReference, 60);
if (! $lock->get()) {
// Lock active: already being processed
return;
}
try {
DB::transaction(function () {
$payment = Payment::where('reference', $this->eventReference)->first();
if ($payment && $payment->status === 'paid') {
return; // Already processed
}
$payment->update([
'status' => 'paid',
'paid_at' => now(),
]);
});
} finally {
$lock->release();
}
}
}
Summary
- Job Batching organizes bulk operations and lifecycle hooks.
- Check
$this->batch()?->cancelled()in batchable jobs to prune work early upon failures. - Design all workers and webhooks to be idempotent using Atomic Locks and state checks before committing database transactions.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.