All skills
asyrafhussin avatar

/laravel-queues

@2631530

Laravel 13 queue and job patterns — driver choice, job design (idempotency, ShouldQueue, model serialisation), retry and failure handling, worker scaling, Bus batching and chaining, Horizon when warranted, queue testing. Use when designing async jobs, scheduling background work, configuring Horizon, debugging stuck jobs, or auditing queue health. Triggers on "Laravel queue", "Laravel job", "background job", "Horizon setup", "failed jobs", "Bus batch", "queue worker tuning".

Use this Skill: https://skilld.dev/gh/asyrafhussin/agent-skills/laravel-queues

This session only. Nothing lands on disk.

rulesdesign-pass-ids-not-models.md

≈1.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Pass IDs to Jobs, Not Eloquent Models

Impact: CRITICAL (Serialised models go stale; large payloads inflate the queue; SerializesModels refresh-from-DB has subtle traps)

Constructor arguments to a queued job are serialised, written to the queue (Redis/MySQL), then deserialised on the worker. Passing an Eloquent model means:

  • The entire serialised model (potentially with eager-loaded relations) lives in the queue payload
  • SerializesModels trait refreshes the model from the DB on the worker — which sounds good, but means the model state at dispatch is discarded; any unsaved changes are lost
  • If the model is deleted between dispatch and execution, the job throws ModelNotFoundException
  • For collections, you ship the whole collection through Redis

The fix is mechanical: pass IDs, refetch in handle().

Incorrect

// ❌ Pass the whole model — bloated payload, stale state

class SendOrderConfirmation implements ShouldQueue
{
    use Dispatchable, Queueable, SerializesModels;

    public function __construct(public readonly Order $order) {}

    public function handle(): void
    {
        // SerializesModels has refreshed $this->order from the DB.
        // Any unsaved state from the dispatcher is gone.
        Mail::to($this->order->user)->send(new OrderConfirmation($this->order));
    }
}

// Dispatch:
$order = Order::find(1)->load(['items', 'shipments', 'user.addresses']);
SendOrderConfirmation::dispatch($order);
// → Redis payload contains the order, its items, its shipments, its user, and the user's addresses.
// → Worker discards all of that and refetches `Order::find(1)` (without the relations).
// → 30KB payload for no benefit.
// ❌ Pass a collection
class ProcessOrders implements ShouldQueue
{
    use Dispatchable, Queueable, SerializesModels;

    public function __construct(public readonly Collection $orders) {}
    // → Every order in the collection is serialised individually.
    // → A 1000-order batch dispatches a 1MB+ payload to Redis.
}

Correct

// ✅ Pass IDs; refetch in handle()

class SendOrderConfirmation implements ShouldQueue
{
    use Dispatchable, Queueable;

    public function __construct(public readonly int $orderId) {}

    public function handle(): void
    {
        $order = Order::with(['user', 'items'])->findOrFail($this->orderId);
        Mail::to($order->user)->send(new OrderConfirmation($order));
    }
}

// Dispatch:
SendOrderConfirmation::dispatch($order->id);    // 8-byte payload
// ✅ Pass array of IDs for collection use cases
class ProcessOrders implements ShouldQueue
{
    use Dispatchable, Queueable;

    /** @param int[] $orderIds */
    public function __construct(public readonly array $orderIds) {}

    public function handle(): void
    {
        Order::whereIn('id', $this->orderIds)
            ->with('user')
            ->chunkById(100, fn ($chunk) => $chunk->each(fn ($o) => $this->processOne($o)));
    }
}

Why it works:

  • Payload size is bounded (a list of integers)
  • handle() decides exactly what to load (just user, not 5 eager-loaded relations)
  • If the row was deleted between dispatch and execution, findOrFail throws — caller decides via failed() how to handle (often correct behaviour)
  • Idempotency is easier — the ID is the natural key

When passing the model IS acceptable

Two narrow cases:

  1. Tiny, immutable value objects that don't have an id (e.g., a Money DTO, a config struct). These serialise cheaply and have no DB state to drift.
  2. Implicit Route Model Binding in route definitions — not a queue concern.

If the value has a database id, pass the id.

The SerializesModels trait clarified

SerializesModels doesn't "serialise" the model in the usual sense. At dispatch time, it stores the model's class name + primary key. At handle time, it issues a fresh find($id) (i.e., reads the current state from the DB). This is why:

  • Stale state from the dispatcher is discarded — a feature for some cases, a footgun for others
  • The model must still exist when the job runs (ModelNotFoundException if deleted)
  • You can use SerializesModels AND pass the ID — but at that point, just pass the ID directly without the trait, and call findOrFail yourself in handle()

For new code: pass IDs, drop SerializesModels unless there's a specific need.

Detection

# Find job constructors that accept Eloquent models or collections
grep -rEnB1 '__construct\(' --include='*.php' app/Jobs/ | \
  grep -E 'Model|Eloquent|Collection|EloquentBuilder|\\\\App\\\\Models\\\\'

Reference: Laravel 13 — Queues: Class Structure · Laravel 13 — Eloquent: Serialization

Source: SKILL.md on GitHub

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The analyzed skill contains high-quality reference guidelines and checklists for configuring and auditing Laravel 13 queue structures and background jobs. No security risks, malicious patterns, or vulnerabilities were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at 2631530. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated 5 months ago
metadata
{
  "author": "agent-skills",
  "version": "1.0.0"
}

README badge

README badge for asyrafhussin/agent-skills/laravel-queues