Laravel Queues for Reliable Business Automation

  • Laravel
  • Backend
  • Automation

How to use Laravel queues for business automation: idempotent jobs, retries with backoff, failed-job handling, unique jobs and worker monitoring.

Business automation is mostly background work: send the approval email, post the invoice to the ERP, generate the month-end report. Laravel's queue system handles this well, but a job that works in testing can still cause trouble in production when it runs twice or fails halfway. These are the practices I rely on.

What belongs in a queue

  • Anything that calls another system: email, SMS, payment or ERP APIs.
  • Anything slow: PDF and Excel generation, imports, image processing.
  • Anything that can be retried later without the user waiting.

Write jobs that are safe to run twice

A queue guarantees a job runs at least once, not exactly once. A worker can crash after posting an invoice but before marking the job done, and the job runs again. Design for that: check whether the work is already complete, or send an idempotency key to the other system.

class PostInvoiceToErp implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 5;
    public $backoff = [30, 120, 600];

    public function __construct(public Invoice $invoice) {}

    public function handle(ErpClient $erp): void
    {
        if ($this->invoice->erp_document_no) {
            return; // already posted
        }

        $documentNo = $erp->postInvoice($this->invoice, key: 'invoice-'.$this->invoice->id);

        $this->invoice->update(['erp_document_no' => $documentNo]);
    }
}

Retry with backoff

Most failures are temporary: a timeout, a locked row, a service restarting. $tries sets how many attempts a job gets and $backoff sets the wait between them. Increasing waits give the other system time to recover instead of hammering it.

Dispatch after the transaction commits

If you dispatch a job inside a database transaction, a fast worker can pick it up before the transaction commits and fail to find the record. Call ->afterCommit() on the dispatch, or set after_commit to true in the queue connection configuration.

Prevent duplicates and overlaps

Implement ShouldBeUnique when only one copy of a job should be waiting, such as recalculating a customer's balance. Use the WithoutOverlapping job middleware when two jobs for the same record must not run at the same time.

Treat failed jobs as a work list

When a job uses all its attempts, Laravel stores it in the failed_jobs table. Add a failed() method to notify someone, review the table daily and retry with php artisan queue:retry once the cause is fixed. A failed job nobody looks at is a business transaction that silently did not happen.

Run workers properly

  • Keep queue:work alive with Supervisor or systemd, and restart workers on every deploy with queue:restart.
  • Use separate queues for urgent and heavy work so a large export does not delay notifications.
  • Monitor queue length and wait time. With Redis, Laravel Horizon provides this out of the box.

Queues are one part of keeping an application responsive. The rest is in Laravel performance tuning. For a real use case, see how notifications fit into an approval workflow engine.

Common questions

When should I use a queue in Laravel?

Use a queue for work that is slow or depends on another system, such as sending email, calling external APIs, generating reports or processing imports, so the user does not wait for it.

Can a Laravel queued job run more than once?

Yes. Queues deliver at least once, so a job can repeat after a crash or timeout. Jobs should be idempotent: they check whether the work is already done or use an idempotency key.

What happens to a Laravel job that keeps failing?

After its allowed attempts it is recorded in the failed_jobs table. You can inspect it, fix the cause and retry it with php artisan queue:retry.