When should work leave the HTTP request? Sending a receipt, resizing an uploaded image, or synchronizing a CRM record can take longer than the response a v...
When should work leave the HTTP request?
Sending a receipt, resizing an uploaded image, or synchronizing a CRM record can take longer than the response a visitor expects. Laravel queues move that work into a job processed by a worker. The controller can respond after it has committed the business record and dispatched the job. That shorter request is useful, but it introduces a new responsibility: the job may run later, fail, and be attempted again.
This guide covers a payment receipt example. The same design applies to exports, webhooks, document processing, and notification delivery. It assumes a Laravel application with a configured queue connection and a durable record representing the operation.
Define one business outcome per job
A job should accept the identifier of a persisted record, then reload the current record when it runs. Avoid serializing a large mutable object or assuming the state at dispatch time is still current. For a receipt, the job might receive an invoice ID and verify that the invoice is paid before contacting the mail provider.
Keep the side effect behind one clear boundary. If the job both sends mail and updates a remote CRM, a retry after the mail succeeds but the CRM fails can send the mail twice. Separate those operations into jobs with their own status and retry policy, or record progress for each side effect.
Make retries safe with an idempotency key
Queues generally provide at least once processing behavior. A worker can finish an external request and crash before acknowledging the job. The queue will try again. Treat duplicate execution as an ordinary condition.
Store a stable key such as "invoice:123:receipt:v1" with a unique database constraint. Before the side effect, check whether that operation has already completed. If the external provider supports idempotency keys, send the same key with every attempt. A local completed flag alone cannot guarantee exactly once delivery when the remote call succeeds and the process dies before the flag is saved; provider support or reconciliation is necessary for that boundary.
For internal database changes, use a transaction and a unique constraint. The unique index is the final protection against two workers racing to create the same operation.
Dispatch only after the record is committed
If a controller dispatches a job inside a transaction, a fast worker may read the database before the transaction commits. Laravel supports after commit dispatching. Use it when the job needs data created in the current transaction:
DB::transaction(function () use ($invoice) {
$invoice->markPaid();
SendReceipt::dispatch($invoice->id)->afterCommit();
});
The example is conceptual: markPaid() must be implemented by the application. If the transaction rolls back, the receipt job should not be queued. Test this against the queue connection used in production, because the sync connection can hide timing problems.
Configure attempts, backoff, and timeouts
Retry transient failures such as provider timeouts. Do not retry permanent failures, such as an invalid recipient or a missing authorization grant, without changing the input. Set a finite attempt count and an increasing backoff when the provider may be temporarily unavailable.
The worker timeout must be shorter than the connection's retry_after setting. Otherwise another worker can start the same job while the first is still running. Review the actual queue configuration and the longest expected provider request before choosing numbers. The official Laravel queue documentation explains attempts, backoff, failed jobs, and this timeout relationship.
Recover failed work
Record a correlation ID, operation key, invoice ID, provider response code, and exception context. Avoid logging personal data or credentials. Monitor the failed jobs table and alert on a meaningful rise in failures. A retry command is useful after a transient outage, but first inspect why the job failed: retrying a bad payload repeatedly does not repair it.
Make the operation visible in the admin interface when a human needs to intervene. Useful states include pending, processing, completed, and requires attention. A status is especially important when a job's result affects money, account access, or contractual communication.
Test the failure boundary
The important tests are not just "job dispatched." Simulate two executions of the same job and verify that the unique operation is created once. Simulate provider success followed by a local exception and verify what a retry will do. Test a rolled back transaction and confirm no job runs. Test a permanent provider rejection and confirm the operation reaches an actionable failure state.
For a real integration, use a sandbox provider and confirm its idempotency behavior. A mocked provider can prove your code sends the key, but cannot prove the provider honors it.
Common mistakes
- Dispatching before database commit makes a worker read missing or stale records.
- Using an in-memory flag as the only duplicate guard fails across workers and restarts.
- Logging a full request payload can leak customer information.
- Retrying all exceptions with no attempt limit hides permanent errors and increases cost.
- Combining unrelated side effects in one job makes partial success difficult to recover.
Conclusion
Queueing makes a request faster only when the background operation is observable and recoverable. Start with a durable operation record, a stable idempotency key, after commit dispatching, bounded retries, and a tested failure path. The related web and SaaS service covers the application architecture around these jobs.
References