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.

rulesretry-failed-method.md

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

failed(Throwable $e) — Handle Permanent Failures

Impact: HIGH (Without failed(), permanent failures are silent; customers see orders stuck in 'pending')

When a job exhausts all $tries (or hits $maxExceptions), Laravel writes the job to failed_jobs and stops. If you don't implement failed(Throwable $e), that's the end of it — the side effect is incomplete, the row in your DB is in a half-state, the customer never finds out.

failed() is your hook for terminal failure handling: revert state, notify, refund, alert. Implement it for every job whose terminal failure has a meaningful response.

Incorrect — no failed() handler

// ❌ Job permanently fails; the order is left in 'pending' status forever

class ChargeOrder implements ShouldQueue
{
    use Dispatchable, Queueable;

    public int $tries = 5;

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

    public function handle(StripeGateway $stripe): void
    {
        $order = Order::findOrFail($this->orderId);
        if ($order->status === 'paid') return;

        $charge = $stripe->charge($order);
        $order->markPaid($charge->id);
    }
    // No failed() method!
}

// What happens:
// 1. Stripe API down for 1 hour
// 2. All 5 attempts fail
// 3. Job written to failed_jobs
// 4. Order stays in 'pending' status indefinitely
// 5. Customer emails support 2 days later: "you charged me, why is my order still pending?"
//    (Actually you DIDN'T charge them — but no one knows because nothing surfaced)

Correct — implement failed()

// ✅ Terminal-failure handler reverts state and surfaces the problem

class ChargeOrder implements ShouldQueue
{
    use Dispatchable, Queueable;

    public int $tries = 5;

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

    public function handle(StripeGateway $stripe): void { /* … as before */ }

    public function failed(Throwable $e): void
    {
        $order = Order::find($this->orderId);
        if (!$order) return;   // order deleted; nothing to do

        $order->markPaymentFailed($e->getMessage());

        // Surface to customer support / on-call
        report($e);   // → Sentry / Bugsnag

        // Notify customer of the problem (with a retry link or contact info)
        Mail::to($order->user)->send(new PaymentFailedNotification($order, $e));

        // Slack / paging — payment failures matter
        Notification::route('slack', config('app.alerts_webhook'))
            ->notify(new OrderPaymentFailed($order));
    }
}

What failed() typically does:

  1. Revert / update state — mark the row as failed so downstream queries see the correct state
  2. Report the exception — report($e) sends to Sentry / Bugsnag / Rollbar
  3. Notify users — for user-facing failures (payment, signup), email or in-app notification
  4. Alert the team — for high-stakes failures, Slack / PagerDuty / OpsGenie
  5. Optionally re-dispatch later — for retries beyond the queue's policy (e.g., "try again tomorrow")

What failed() should NOT do

  • Retry the same job — that's what the queue's retry mechanism is for. If you need to retry differently, dispatch a different job (RetryChargeOrder::dispatch(...) next day).
  • Throw exceptions — failed() shouldn't fail. Wrap in try/catch if any of its operations could throw.

Per-job vs global failure listener

You can also wire a global JobFailed event listener (covered in config-failed-storage) for cross-cutting concerns (Sentry reporting, metrics). The per-job failed() is for job-specific semantics (order-specific state revert).

Best practice: both.

  • Global listener → reports to Sentry, increments a Prometheus counter, sends to Slack
  • Per-job failed() → job-specific side-effect reversal (mark order as failed, refund the hold, etc.)

Detection

# Jobs whose handle() touches external services but have no failed() method
for f in app/Jobs/*.php; do
  has_external=$(grep -qE 'Http::|Mail::|Stripe::|Notification::' "$f" && echo y || echo n)
  has_failed=$(grep -qE 'public function failed\b' "$f" && echo y || echo n)
  if [ "$has_external" = "y" ] && [ "$has_failed" = "n" ]; then
    echo "MISSING failed(): $f"
  fi
done

Reference: Laravel 13 — Queues: Cleaning Up After Failed Jobs · Laravel 13 — Queues: Job Failed Event Listeners

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