Laravel cookbook · Intermediate
Make queued jobs reliable with retries, backoff and uniqueness
Jobs fail on temporary errors, run twice for the same record, or hang forever.
Approach
Configure tries, exponential backoff and a timeout with job attributes, make the job unique per record so it isn't
queued twice, and handle final failures in failed(). Keep jobs idempotent.
Solution
<?php
// app/Jobs/SyncInvoiceToAccounting.php
namespace App\Jobs;
use App\Models\Invoice;
use App\Services\AccountingClient;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\UniqueFor;
use Throwable;
#[Tries(5)]
#[Backoff([10, 60, 300, 900])]
#[Timeout(30)]
#[UniqueFor(600)]
class SyncInvoiceToAccounting implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public function __construct(public Invoice $invoice) {}
public function uniqueId(): string
{
return (string) $this->invoice->id;
}
public function handle(AccountingClient $accounting): void
{
if ($this->invoice->accounting_id) {
return; // already synced: safe to run twice
}
$id = $accounting->createInvoice($this->invoice);
$this->invoice->update(['accounting_id' => $id]);
}
public function failed(?Throwable $exception): void
{
$this->invoice->update(['sync_status' => 'failed']);
report($exception);
}
}
<?php
use App\Jobs\SyncInvoiceToAccounting;
SyncInvoiceToAccounting::dispatch($invoice)->afterCommit()->onQueue('integrations');
Pitfalls
- Dispatching inside a database transaction without
afterCommit(), so the job runs before the data exists. - Timeouts longer than the queue connection's
retry_after, causing duplicate processing. - Catching exceptions inside
handle()and returning normally, which hides failures from retries.
Variations
- Add the
ThrottlesExceptionsmiddleware to back off when a third-party API is down. - Use Laravel Horizon to monitor Redis queues, failure rates and wait times.
Further reading: Queues
Reference: https://laravel.com/docs/13.x/queues
More Laravel recipes
- Authorize actions with policies
- Cache expensive data with stale-while-revalidate
- Call external APIs with timeouts, retries and fakes
- Catch N+1 queries with strict Eloquent mode
- Delete old records automatically with prunable models
- Dispatch events and listeners after the transaction commits
- Issue scoped API tokens with Sanctum
- Prevent race conditions with transactions and row locks