Watch Out: Queued Events Can Fail if the Record Gets Deleted

๐Ÿ“– 3 minutes read

The Problem: Race Condition Between Job Queuing and Processing

You fire off a queued event with an Eloquent model, everything looks good, but hours later your queue worker crashes with a ModelNotFoundException. What happened?

When you pass an Eloquent model to a queued event or listener, Laravel doesn’t serialize the entire model object. Instead, it just stores the model’s class name and its ID. When the queue worker picks up the job later, it tries to refetch that model using firstOrFail().

The problem? If the record gets deleted between queuing and processing, your job fatally crashes.

Example: The Classic Gotcha

// In your controller or service
event(new OrderUpdated($order, $changes));

// Meanwhile, in OrderUpdated event class...
class OrderUpdated
{
    use SerializesModels;
    
    public $order;  // This gets serialized as: Order::class, id: 123
    
    public function __construct(Order $order, array $changes)
    {
        $this->order = $order;
        $this->changes = $changes;
    }
}

// Later, when the queue worker processes this job...
// Laravel does: Order::findOrFail(123)
// If order #123 was deleted: ModelNotFoundException!

This is especially common in high-traffic applications where records get created and deleted quickly โ€” cancelled transactions, temporary data, race conditions between user actions and background cleanup jobs.

Solution 1: Pass Only What You Need

Instead of serializing the entire model, extract just the data you’ll actually need:

class OrderUpdated
{
    public $orderId;
    public $changes;
    public $customerEmail;
    
    public function __construct(Order $order, array $changes)
    {
        // Extract primitives, not objects
        $this->orderId = $order->id;
        $this->customerEmail = $order->customer->email;
        $this->changes = $changes;
    }
}

Now if the order gets deleted, your listener can handle it gracefully โ€” maybe just log “order no longer exists” instead of crashing.

Solution 2: Handle Missing Models Gracefully

If you do serialize the model, check for null in your listener:

class SendOrderUpdateEmail
{
    public function handle(OrderUpdated $event)
    {
        // Laravel sets the property to null if model can't be restored
        if (!$event->order) {
            Log::info('Order no longer exists, skipping notification');
            return;
        }
        
        // Safe to use here
        Mail::to($event->order->customer)->send(new OrderChanged($event->order));
    }
}

Solution 3: Use Custom Restoration Logic

For more control, override the model restoration behavior:

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;

class OrderUpdated implements ShouldQueue
{
    use SerializesModels {
        __sleep as protected traitSleep;
        __wakeup as protected traitWakeup;
    }
    
    public $order;
    
    public function __wakeup()
    {
        $this->traitWakeup();
        
        // If model restoration failed, log and set to null
        if (!$this->order) {
            logger()->warning('Order model could not be restored for queued event');
        }
    }
}

When to Worry About This

This pattern matters most when:

  • Records are short-lived โ€” temporary carts, pending transactions, OTPs
  • Users can delete things โ€” if delete actions fire cleanup jobs that might race with notification jobs
  • You have cascading deletes โ€” parent record deletion triggers child deletions while jobs are in flight
  • Queue delays are significant โ€” if your queue is backed up, more time = more opportunity for deletions

Takeaway

Queued events with Eloquent models are convenient, but they assume the record still exists when the job runs. For critical paths, consider passing primitives instead of models, or add defensive checks in your listeners. Your queue workers will thank you.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *