Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series.
Queued event listeners are great for moving slow work off the request. But there is an easy way to waste your queue: pushing a listener for every event when most of the time it will run, check a condition, find it is not met, and do nothing. Laravel has a clean way to skip queueing in that case.
The setup
Picture an online store that gives a gift coupon to customers whose total purchases pass 10,000. On every purchase a NewPurchase event fires, with several listeners.
// a queued listener
class SendGiftCoupon implements ShouldQueue
{
public function handle(NewPurchase $event): void
{
$customer = $event->customer;
if ($customer->purchases >= 10000 && ! $customer->wasGifted()) {
$coupon = Coupon::createForCustomer($customer);
Mail::to($customer)->send(new CouponGift($customer));
}
}
}
Because it implements ShouldQueue, the listener is pushed to the queue instead of running in the request, so the customer hits a nice success screen while the coupon work happens in the background. Good so far.
The waste
Here is the problem. How many customers actually cross the 10,000 mark? A tiny fraction. Yet a job is queued on every single purchase, and almost all of those jobs boot up, check the condition, find it false, and exit having done nothing. On a busy store that is thousands of pointless jobs clogging the queue, potentially delaying jobs that actually matter.
The fix: shouldQueue on the listener
Laravel lets a queued listener decide whether it is even worth queueing, by adding a shouldQueue method. Return false and the listener is simply not pushed to the queue at all.
class SendGiftCoupon implements ShouldQueue
{
public function shouldQueue(NewPurchase $event): bool
{
return $event->customer->purchases >= 10000
&& ! $event->customer->wasGifted();
}
public function handle(NewPurchase $event): void
{
$coupon = Coupon::createForCustomer($event->customer);
Mail::to($event->customer)->send(new CouponGift($event->customer));
}
}
The condition now runs cheaply and synchronously when the event fires. Only the handful of purchases that actually qualify ever create a queued job, and handle() no longer needs the guard because it only runs when it should.
Why this is better than guarding inside handle
Both approaches produce the right outcome, but shouldQueue is the right tool when the condition is rarely met. Guarding inside handle() still pays the full cost of serializing, storing, transporting, and booting a job just to immediately return. shouldQueue makes the decision before any of that, so your queue only ever holds jobs that will do real work. Reserve the in-handle guard for cheap checks; use shouldQueue when most events should not produce a job.
Wrapping up
A queued listener does not have to be queued unconditionally. Add a shouldQueue method and Laravel checks it before pushing anything, so you never fill the queue with jobs that exist only to find out they have nothing to do. When most events will not qualify, this keeps your queue lean and your important jobs moving.
More in the series: designing reliable, self-contained jobs and how Laravel prepares a job for the queue. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.