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.

Create a free account to save this page, take notes and track your progress. Sign in

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 ThrottlesExceptions middleware 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

Was this page helpful?