In Laravel, how do you queue an event listener and control its connection, queue name, delay and whether it queues at all?
answer
- one interface on the listener
- the event is serialized with it
- viaConnection / viaQueue / withDelay
- #[Queue] attribute or $queue property
- shouldQueue() false skips it entirely
basics
~20 sImplement ShouldQueue on the listener (make:listener --queued). The dispatcher then pushes a job carrying the event; viaConnection, viaQueue and withDelay methods or the #[Connection], #[Queue] and #[Delay] attributes route it, and shouldQueue($event) returning false skips the listener.
solid answer
~40 sAdd `implements ShouldQueue` to the listener, or generate it with `make:listener --queued`. When the event fires, the dispatcher does not call `handle()`; it wraps the listener class, method and event in a `CallQueuedListener` job and pushes it, so a worker runs `handle()` later. The event is serialized, and `SerializesModels` stores model identifiers that the worker re-queries. In Laravel 13 routing is set by `#[Connection('redis')]`, `#[Queue('listeners')]` and `#[Delay(60)]` (or the older `$connection`, `$queue`, `$delay` properties), and runtime methods `viaConnection()`, `viaQueue()` and `withDelay($event)` win over both. A `shouldQueue(OrderPlaced $event): bool` method runs in the request at dispatch time; returning `false` means the listener does not run at all, not that it runs synchronously. `InteractsWithQueue` adds `release()` and `delete()` inside `handle()`.
code
php · 23 lines<?php
namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Mail\OrderConfirmation;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Queue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail;
#[Connection('redis')]
#[Queue('mail')]
class SendOrderConfirmation implements ShouldQueue
{
use InteractsWithQueue;
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->customer_email)->send(new OrderConfirmation($event->order));
}
}go deeper
Recall that implementing ShouldQueue (or make:listener --queued) queues a listener and that a worker must be running.
Explain the CallQueuedListener job, event serialization with SerializesModels, the precedence of runtime methods over attributes and properties, and what shouldQueue() returning false really does.
Decide which listeners of a fan-out stay synchronous, route slow ones to dedicated queues, and anticipate missing-model and stale-data failures in the worker.
Frame queue topology for listeners across a product: isolating mail and analytics traffic, keeping critical paths synchronous, and setting conventions so listener routing stays predictable.
## Why queue a listener A synchronous listener runs inside the request that fired the event, so a slow listener (sending mail, calling an HTTP API, writing to an analytics store) makes the customer wait and can fail the checkout. A **queued listener** moves that work to a background worker. In the order fan-out, `SendOrderConfirmation` and `RecordOrderAnalytics` are natural candidates, while `ReserveInventory` may need to stay synchronous if the order is not valid without it. ## Turning it on 1. Implement `Illuminate\Contracts\Queue\ShouldQueue` on the listener class, or generate it with `php artisan make:listener SendOrderConfirmation --event=OrderPlaced --queued`. 2. Make sure a queue connection is configured; the Laravel 13 skeleton sets `QUEUE_CONNECTION=database`. 3. Run a worker with `php artisan queue:work`, otherwise the jobs wait in the table forever. With the `sync` connection the "queued" listener runs immediately in the same process. The dispatcher checks the interface by reflection. For a queued listener it builds a `CallQueuedListener` job holding the listener class, the method name and a clone of the event, and pushes it. The worker later resolves the listener from the container and calls `handle()`. ## What happens at dispatch, step by step When `OrderPlaced::dispatch($order)` reaches a listener that implements `ShouldQueue`, the dispatcher: 1. checks the listener class for the `ShouldQueue` interface by reflection, without calling `handle()` 2. resolves the listener once and calls its `shouldQueue($event)` method, if it has one, to decide whether to continue 3. builds a `CallQueuedListener` job from the class name, the method name and a clone of the event, copying options such as tries and timeout from the class 4. works out the connection, the queue name and the delay 5. pushes the job, or pushes it with a delay when one is set Nothing in that sequence runs the listener's business logic, which is why the request returns quickly and why errors in `handle()` show up in the worker's logs rather than in the response. ## What travels with the job - the **event object is serialized**, so everything on it must be serializable - with `SerializesModels` on the event, an Eloquent model is stored as its class and key and **re-queried** in the worker; if the row is gone the restore throws `ModelNotFoundException` - the listener's own properties are **not** carried over; the worker builds a fresh instance through the container, so constructor dependencies are injected there - the job's options (retries, backoff, timeout, uniqueness) are read from the listener class when the job is created ## Choosing connection, queue and delay | Setting | Declarative (Laravel 13) | Older property | Runtime method | |---|---|---|---| | Connection | `#[Connection('redis')]` | `public $connection` | `viaConnection()` | | Queue name | `#[Queue('listeners')]` | `public $queue` | `viaQueue()` | | Delay | `#[Delay(60)]` | `public $delay` | `withDelay(OrderPlaced $event)` | The runtime method, when present, always wins over the attribute or property. If none of them names a connection or queue, the dispatcher falls back to any queue route registered for the class and then to the connection's default queue. The runtime methods receive the event, so `withDelay()` can return `0` for high-priority orders and `60` for the rest. ## Deciding whether to queue at all A `shouldQueue()` method lets the event's data decide: ```php public function shouldQueue(OrderPlaced $event): bool { return $event->order->total >= 5000; } ``` It is evaluated **in the request, at dispatch time**. Returning `false` means the listener is **skipped entirely** for that event; Laravel does not fall back to running it synchronously. Anything that must still happen for small orders belongs in a different listener. ## Other controls on the listener - `use InteractsWithQueue` gives `handle()` access to `$this->release(30)` to put the job back and `$this->delete()` to drop it - `ShouldQueueAfterCommit` queues the listener only after the surrounding database transaction commits - closure listeners can be queued by wrapping them in `Illuminate\Events\queueable()`, which also offers `onConnection()`, `onQueue()` and `delay()` - a queued listener's return value is ignored, so returning `false` from it cannot stop later listeners the way a synchronous one can Retry counts, backoff and the `failed()` hook of queued listeners follow the queue system's failure rules and are a separate subject, as is running and supervising the workers themselves.
- How do you queue a closure listener registered with Event::listen?Wrap the closure in `Illuminate\Events\queueable()`: `Event::listen(queueable(function (OrderPlaced $event) { ... })->onQueue('analytics'))`. The returned object also offers `onConnection()`, `delay()` and `catch()` for a failure callback. The closure is serialized onto the queue, so it must not capture anything that cannot be serialized.
- What happens to a queued listener when QUEUE_CONNECTION is sync?The job is still created, but the `sync` connection executes it immediately in the current process, so the listener runs during the request and its exceptions surface there. It is handy in tests or local work, but it gives none of the latency benefit of a real worker.
- Does a property set in a queued listener's constructor survive into handle() on the worker?No. At dispatch the job stores the listener's class name, the method and the event, not the listener object. The worker builds a new instance through the container, so constructor work runs again there; per-dispatch data must travel on the event.
saying these in an interview costs you the question
- Returning false from shouldQueue() makes the listener run synchronously instead.
- A queued listener runs handle() in the request and only defers the result.
- State set on the listener instance at dispatch time is available in the worker.
- Queued listeners run without a queue worker because the event dispatcher executes them.
- A #[Queue] attribute on the listener overrides whatever viaQueue() returns.