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-fail-on-timeout.md

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

#[FailOnTimeout] — Don't Burn Attempts on Hangs

Impact: HIGH (A hung HTTP call kills 5 attempts at 30s each before failing — 2.5 min of wasted worker time per stuck job)

Without #[FailOnTimeout], a job that exceeds $timeout is released back to the queue and tries again — the same way as a job that threw an exception. For a hung HTTP call (third-party API not responding), the second attempt will likely hang too. And the third. With $tries = 5 and $timeout = 30, a single stuck dispatch consumes 150 seconds of worker time across 5 attempts.

#[FailOnTimeout] tells Laravel: "if this job times out, it's done. Mark it failed; don't retry." That's almost always what you want for timeout-prone jobs.

Incorrect — default behaviour wastes attempts

// ❌ Without FailOnTimeout, timeouts are released and retried

#[Backoff([1, 5, 30])]
class FetchOrderFromShopify implements ShouldQueue
{
    use Dispatchable, Queueable;

    public int $tries = 5;
    public int $timeout = 30;

    public function handle(): void
    {
        // Shopify is down; this HTTP call hangs for the full 30s timeout
        $data = Http::get("https://api.shopify.com/orders/{$this->orderId}")->json();
        // …
    }
}

Timeline of a hung job:

Attempt Outcome Wait before next Time elapsed
1 Times out after 30s 1s backoff 31s
2 Times out after 30s 5s backoff 66s
3 Times out after 30s 30s backoff 126s
4 Times out after 30s (no more) 156s
5 Times out after 30s — 186s

3 minutes of worker time burned. Meanwhile real jobs are waiting behind it.

Correct — #[FailOnTimeout] for I/O-bound jobs

// ✅ Timeout = immediate failure; don't burn more attempts

use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\FailOnTimeout;

#[Backoff([1, 5, 30])]
#[FailOnTimeout]
class FetchOrderFromShopify implements ShouldQueue
{
    use Dispatchable, Queueable;

    public int $tries = 5;
    public int $timeout = 30;

    public function handle(): void
    {
        // Shopify hangs → job dies at 30s → failed() called → no further retries
        $data = Http::timeout($this->timeout - 1)         // belt-and-braces: HTTP-level timeout too
            ->get("https://api.shopify.com/orders/{$this->orderId}")
            ->json();
        // …
    }

    public function failed(Throwable $e): void
    {
        report($e);   // alert; investigate Shopify status
    }
}

Now the same hung job burns 30 seconds, hits failed(), and the worker moves on. The other jobs queued behind don't suffer.

When NOT to use #[FailOnTimeout]

For jobs where the timeout is set generously and a single timeout might just mean "the worker was busy" rather than "the operation is genuinely stuck":

  • Long-running data processing (PDF generation, video transcoding)
  • Operations that get faster on subsequent retries (caches warm up, etc.)
  • Jobs where the timeout is a generous upper bound rather than a strict deadline

For those, let the timeout release the job for retry. But pair with a sensible $tries (3 is fine) and #[Backoff] so you don't loop tightly.

Setting the HTTP-level timeout to less than the job timeout

A common bug: the job's $timeout = 30 but the HTTP client has no timeout, so the call hangs longer than the job, the job killer interrupts mid-write, and you get partial state. Always:

$response = Http::timeout($this->timeout - 1)   // shave 1s for cleanup
    ->retry(0)                                   // disable client-level retry; let the job's retry handle it
    ->post('https://api.example.com/charge', $payload);

Cross-references the technical-debt's defensive-missing-real rule.

Detection

# Jobs that touch external services without #[FailOnTimeout]
for f in app/Jobs/*.php; do
  has_io=$(grep -qE 'Http::|Stripe::|Mail::' "$f" && echo y || echo n)
  has_fail_on_timeout=$(grep -qE '#\[FailOnTimeout\]' "$f" && echo y || echo n)
  has_timeout=$(grep -qE '\$timeout\s*=' "$f" && echo y || echo n)

  if [ "$has_io" = "y" ] && [ "$has_fail_on_timeout" = "n" ]; then
    echo "CONSIDER #[FailOnTimeout]: $f (touches I/O, no FailOnTimeout)"
  fi
done

# Jobs with no $timeout at all (uses worker default of 60s)
for f in app/Jobs/*.php; do
  grep -q 'implements ShouldQueue' "$f" || continue
  grep -qE '\$timeout\s*=|--timeout' "$f" || echo "NO TIMEOUT SET: $f"
done

Reference: Laravel 13 — Queues: Failing on Timeout

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