Laravel queues, events, listeners, notifications, and retries
Status: Complete. Last reviewed 2026-08-27.
Precise mental model
Section titled “Precise mental model”A Laravel queue moves a serialized command from a producer to a worker through a configured connection and named queue. It creates a temporal and process boundary, not a stronger delivery guarantee. A worker reserves an available message, resolves and runs the job, then deletes or acknowledges it after success. If the worker dies, times out, or loses its acknowledgement, the driver can make the message available again. Design every important handler for possible redelivery.
An event records that something happened. Laravel invokes ordinary listeners synchronously during event dispatch; a listener becomes asynchronous only when it implements ShouldQueue. A notification expresses delivery of information to a notifiable recipient through channels such as mail, database, broadcast, or a provider. Jobs, listeners, and notifications can all use the queue, but they communicate different intent.
Scheduling is deliberately owned by Artisan, console commands, and scheduling. Worker deployment, process recycling, and Octane state are covered in Deployment and long-running workers. Generic messaging guarantees and outbox patterns remain under distributed systems and reliability.
Dispatch, serialization, and state freshness
Section titled “Dispatch, serialization, and state freshness”Dispatching constructs a payload containing the job class and serialized job state. Laravel’s queue traits serialize Eloquent models as identifiers and restore them when the job executes, reducing payload size and using then-current database state. That is useful but not a snapshot guarantee. Previously loaded relationships are serialized as relationship names and may be reloaded without the constraints used before dispatch. Use withoutRelations() or Laravel’s WithoutRelations attribute when those graphs are unnecessary, and query the exact data the handler needs.
Pass stable scalar identity when tenant scope, authorization context, or payload compatibility must be explicit. A worker does not inherit request-local tenant state, authenticated user, locale, open transactions, or container mutations. Restore required context before scoped lookup and decide whether authorization is a dispatch-time decision or must be checked again during execution. Avoid secrets in payloads; queued jobs can implement ShouldBeEncrypted when driver storage must not expose their serialized data.
dispatchSync() runs the job in the current process. dispatchAfterResponse() defers work until after the HTTP response has been sent when the server supports it, but still runs in the request process and suits only short work. Normal queued dispatch returns control after publishing to the configured backend. These choices change latency and failure boundaries even when they call the same handler.
Database transactions and durable publication
Section titled “Database transactions and durable publication”A worker may consume a job before the producer’s database transaction commits. It can then miss a new row, observe old state, or act on data that later rolls back. Setting a connection’s after_commit option, calling afterCommit(), or using the corresponding after-commit listener contract delays publication until the outer transaction commits; rollback discards the pending dispatch.
After-commit dispatch solves visibility ordering, not atomic delivery between the database and queue. The process can still fail after the database commit but before the broker accepts the message. When losing the message would violate a business invariant, write an outbox record in the same database transaction and publish it through a retryable relay. Consumers still remain idempotent because the relay or broker may publish more than once.
Be deliberate with nested transactions and testing. The callback waits for the transaction manager’s final commit, and transaction-wrapped tests may never reach that boundary. The Laravel testing reference explains how to prove commit behavior without disabling the production safety mechanism.
Attempts, exceptions, releases, and failure
Section titled “Attempts, exceptions, releases, and failure”A handler that throws is released or failed according to its attempt and exception limits. A handler may also call release() deliberately, as rate-limiting and overlap middleware do. Released attempts still consume attempts, so a low $tries value can exhaust before useful work runs. Use $tries, $maxExceptions, and retryUntil() to express distinct budgets, and use backoff() for delays between attempts. Backoff reduces pressure; jitter and provider-aware rate limits help prevent synchronized retry storms.
Classify errors. Invalid or permanently rejected work should call fail() or throw a non-retryable application exception that the handler converts to failure. Transient network errors should escape so the worker can retry. Do not catch every throwable and return normally: the worker interprets that as success and acknowledges the message. Give outbound HTTP and database calls their own timeouts because the job timeout does not reliably interrupt every blocking I/O operation.
For database and Redis drivers, retry_after controls when a reserved job may be retried. The worker’s --timeout should be several seconds shorter, so a stuck worker is terminated before another worker receives the same reservation. If timeout exceeds the reservation window, two processes can perform the job concurrently. Supervisor or Horizon termination settings must also allow Laravel’s timeout and graceful shutdown policy to work. SQS uses its visibility timeout rather than Laravel’s retry_after setting.
When attempts are exhausted, Laravel records the failure through the configured failed-job provider and emits queue failure events. A job’s failed() method receives the throwable, but Laravel creates a fresh job instance before calling it, so mutations made inside handle() are unavailable there. Failed-job alerts need job identity, tenant, attempt, queue, exception class, and correlation context without leaking payload secrets. Retrying a failed job replays old work against current code and data; make compatibility and operator intent explicit.
Idempotency, uniqueness, and overlap prevention
Section titled “Idempotency, uniqueness, and overlap prevention”Idempotency means repeated handling converges on one valid business outcome. Implement it at the durable effect: a unique database key for an idempotency token, a conditional state transition, a provider idempotency key, or a recorded intent/result that reconciliation can complete. Checking “already done” and then acting is still a race unless the check and claim are atomic or the external system deduplicates the effect.
Laravel’s uniqueness and overlap tools solve narrower coordination problems:
ShouldBeUniqueacquires a cache lock before dispatch and suppresses another job with the same unique key while the lock is held.ShouldBeUniqueUntilProcessingreleases that lock immediately before an attempt starts, allowing later work to queue while processing continues.WithoutOverlappingis job middleware that prevents concurrent execution for a key; a conflicting job is released or discarded according to configuration.
Multi-host deployments need a shared lock-capable cache. Every lock also needs a failure policy and sensible expiry so a crash cannot block work forever. Expiry, manual retries, driver failures, or code paths that bypass dispatch can still produce duplicates, so neither uniqueness nor an overlap lock replaces idempotent state transitions. The retry-aware job example combines an overlap guard with a provider idempotency key and durable completion check.
Direct calls, events, listeners, and notifications
Section titled “Direct calls, events, listeners, and notifications”Use a direct call when the caller requires the result or the step is part of an explicit workflow. Use an event for a fact that may have independent reactions and where the publisher should not know each subscriber. Synchronous listeners still extend the dispatching transaction and failure path; an exception can prevent later listeners from running. Do not hide required ordered business steps behind an unordered fan-out.
A queued listener separates latency and failure but inherits queue delivery concerns. Implement ShouldQueueAfterCommit when its event may be dispatched inside a transaction and the listener needs committed data. Listener methods can choose queue connection, queue name, delay, middleware, retry budget, and conditional queueing. Event discovery and explicit registration are wiring choices; verify the resulting listener map rather than assuming a class was discovered.
Use a notification when the concept is recipient-oriented delivery across channels. A notification implementing ShouldQueue creates queued work for each recipient and channel combination, so fan-out can be much larger than the call site suggests. viaConnections() and viaQueues() can route channels differently; locale and channel-specific routing must be available when the worker runs. A notification is not a durable domain fact merely because it can broadcast or store a database representation.
Chains, batches, and operational recovery
Section titled “Chains, batches, and operational recovery”A job chain expresses sequential dependency: Laravel dispatches the next job after the previous one succeeds, and a failure stops the remaining chain. A batch groups jobs for progress, completion, failure, and cancellation callbacks; its jobs may run concurrently and ordering is not implied. Cancellation is cooperative, so jobs should check batch state or use the skip middleware. allowFailures() changes batch control flow, not whether individual jobs can fail.
Observe queue depth, oldest-job age, processing latency, runtime, attempts, failures, and exhausted retries by connection and queue. Horizon supplies Redis-queue supervision, balancing, metrics, and operational commands, but it does not make handlers idempotent or correct. Separate queues when workload priority or resource profile demands it, then prevent a permanently busy high-priority queue from starving essential lower-priority work.
Workers are long-lived and keep booted code and container state. Deployments must restart them gracefully, and jobs should tolerate old and new releases coexisting during a rollout. That operational lifecycle belongs to the deployment bundle; the application contract here is backward-compatible payloads, observable failures, and safe replay.
Failure timeline: one payment, two captures
Section titled “Failure timeline: one payment, two captures”- A request commits a payment attempt and dispatches
CapturePayment. - Worker A calls the provider successfully, then loses its database connection before recording completion.
- The reservation expires, so worker B receives the same job.
- A uniqueness lock has already expired and an “is captured?” check still returns false.
- Without a provider idempotency key, worker B charges again; retry configuration behaved as designed.
- The repair assigns a durable idempotency key before dispatch, sends it on every provider attempt, records the provider result with an atomic state transition, and reconciles attempts whose external outcome is uncertain.
Current and legacy context
Section titled “Current and legacy context”Current (Laravel 13): queueable jobs support encrypted payloads, model relationship exclusion, unique-job contracts, overlap and throttling middleware, chains and batches, after-commit dispatch, time-based retry limits, and driver-specific worker configuration. Queued listeners and notifications share these queue concerns, while Horizon remains specific to Redis queues.
Common (Laravel 11–12): the same job contracts, SerializesModels, after-commit options, middleware, failed-job storage, event discovery, and notification channels are widely used. Application skeleton organization and discovery configuration vary by release and project history.
Legacy: older applications may run queue:listen, use database queues without tuned reservation settings, serialize large model graphs, dispatch before commit, or rely on unbounded manual retry. Add characterization tests and failure telemetry first, then introduce explicit retry budgets, payload version tolerance, commit-safe publication, idempotency, and supervised queue:work processes incrementally.
Interview practice
Section titled “Interview practice”- LARAVEL-QUEUES-01 — Trace a queued job lifecycle
- LARAVEL-QUEUES-02 — Align timeout and reservation settings
- LARAVEL-QUEUES-03 — Make a retried side effect idempotent
- LARAVEL-QUEUES-04 — Choose a direct call, event, listener, notification, or job
- LARAVEL-QUEUES-05 — Separate uniqueness, overlap prevention, and idempotency
- LARAVEL-QUEUES-06 — Design transaction-safe fan-out and recovery