A listener that calls a slow service belongs on a queue. make:listener NotifyWarehouse --event=OrderPlaced --queued adds ShouldQueue, so the dispatcher pushes a job instead of calling handle(), and the queue attributes of Queues and Scheduling apply:
#[Queue('warehouse')]
#[Tries(3)]
class NotifyWarehouse implements ShouldQueue
{
public function handle(OrderPlaced $event): void
{
$lines = $event->order->items->count();
Log::info("Warehouse picking list for order #{$event->order->id}: {$lines} lines");
}
}After a checkout, the jobs table held one row on the warehouse queue with "maxTries":3 in its payload, and a worker ran it:
$ php artisan queue:work --queue=warehouse --once 2026-09-23 08:55:13 App\Listeners\NotifyWarehouse .................. RUNNING 2026-09-23 08:55:13 App\Listeners\NotifyWarehouse ............. 17.09ms DONE $ tail -n 1 storage/logs/laravel.log [2026-09-23 08:55:13] local.INFO: Warehouse picking list for order #1: 2 lines
SerializesModels put only the order's key in the payload, so the worker reloaded the order and its items. A failed($event, $e) method runs after the last attempt, shouldQueue($event) decides at runtime, and closures queue with Event::listen(queueable(fn (OrderPlaced $e) => ...)). Workers and retries are Queues and Scheduling.
A subscriber groups handlers in one class: CatalogCacheSubscriber's subscribe() returns [OrderPlaced::class => 'forgetAfterSale', ProductRestocked::class => 'forgetAfterRestock']. Those names do not start with handle, so discovery skips them and Event::subscribe(CatalogCacheSubscriber::class) in boot() registers them, as the event:list output above showed.