Queued Event Listeners: Offloading Side Effects with ShouldQueue
You fire an OrderPlaced event, and three listeners respond: one sends a confirmation email, one notifies your Slack channel, one updates an analytics table. All three run synchronously, in the request cycle, before the customer's browser sees a response — even though none of them need to.
The default behavior
class OrderPlaced
{
public function __construct(public Order $order) {}
}
class SendOrderConfirmation
{
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->customer)->send(new OrderConfirmationMail($event->order));
}
}
By default, event(new OrderPlaced($order)) calls every registered listener's handle() method inline, one after another, as part of the current request. If sending that email takes 400ms and hitting the Slack webhook takes another 200ms, your customer's checkout just got 600ms slower for reasons that have nothing to do with confirming their order actually succeeded.
Making a listener queueable
use Illuminate\Contracts\Queue\ShouldQueue;
class SendOrderConfirmation implements ShouldQueue
{
public function handle(OrderPlaced $event): void
{
Mail::to($event->order->customer)->send(new OrderConfirmationMail($event->order));
}
}
That's the entire change. Implement ShouldQueue, and Laravel automatically pushes this listener onto your default queue connection instead of running it inline, the next time the event fires. No change needed anywhere the event is dispatched — the dispatching code has no idea, and shouldn't need to, which listeners are queued and which aren't.
Configuring which connection and queue
use Illuminate\Contracts\Queue\ShouldQueue;
class NotifySlackChannel implements ShouldQueue
{
public string $connection = 'redis';
public string $queue = 'notifications';
public int $tries = 3;
public int $backoff = 15;
public function handle(OrderPlaced $event): void
{
Http::post(config('services.slack.webhook_url'), [
'text' => "New order #{$event->order->id} — \${$event->order->total}",
]);
}
}
Same properties you'd configure on a regular job class — $connection, $queue, $tries, $backoff — all work identically on a queued listener, because under the hood it is dispatched as a job.
What should stay synchronous
Not every listener belongs on a queue. If a listener's job is to validate something and potentially throw an exception that should abort the current operation — checking inventory before confirming an order, say — running it asynchronously means the calling code has already moved on by the time a problem would be discovered. Keep listeners synchronous when their result needs to affect the current request's outcome; queue them when they're a side effect that the customer's response doesn't depend on.
A good rule of thumb: if the answer to "does the user need to see the result of this before the page finishes loading" is no, it's a strong queueing candidate. Confirmation emails, Slack pings, analytics writes, and third-party webhook notifications almost always qualify. Payment authorization and stock checks almost never do.
One thing to double check
Queued listeners re-fetch any Eloquent models on their event via the same SerializesModels mechanism regular jobs use — so if your event carries a model, the listener sees a fresh copy from the database when it eventually runs, not a frozen snapshot from dispatch time. That's usually exactly what you want, but it's worth knowing if you're debugging a listener that seems to be reading data that "shouldn't have changed yet."