Laravel Queues & Jobs
Production-grade queue patterns for Laravel 13 (MySQL + Redis). Contains 20 rules across 6 categories covering driver choice, job design, retry/failure handling, worker scaling, batching/chaining, and testing. Targets the failure modes that pass code review but break under load — silent re-dispatch on retry, models stale on serialisation, missing idempotency on payment jobs, workers leaking memory, and "we forgot ShouldQueue and the request takes 12 seconds in production."
Metadata
- Version: 1.0.0
- Scope: PHP / Laravel 13 + MySQL + Redis (queues), optional Horizon
- Rule Count: 20 rules across 6 categories
- License: MIT
How to Use
When the user asks "design this background job", "audit our queue setup", "why is this job not running", or anything queue-related — work through this skill's rules as a checklist against the relevant files (job classes, config/queue.php, config/horizon.php, supervisor config, the dispatching call sites).
For audit mode, output per-rule verdicts:
- PASS — pattern correctly applied
- FAIL — anti-pattern present (with file:line + fix recommendation)
- N/A — does not apply to this codebase
End with a top-priority fix list (idempotency on payment jobs, missing ShouldQueue, supervisor stopwaitsecs too low — these are the most common production-bite issues).
When to Apply
Reference this skill when:
- Writing a new background job (
php artisan make:job) - Reviewing a PR that adds or modifies a job class
- Setting up queue workers on a new server (Forge / Vapor / bare server)
- Configuring
config/queue.phporconfig/horizon.php - Debugging a stuck, looping, or repeatedly-failing job
- Choosing between
dispatch(),dispatchSync(),dispatchAfterResponse(),Bus::batch(),Bus::chain() - Adding queue testing (
Queue::fake(),Bus::fake()) - Deciding whether to adopt Horizon
Step 1: Detect Queue Setup
Inspect:
| File | What to learn |
|---|---|
| config/queue.php | Default connection (QUEUE_CONNECTION env), failed-jobs storage, after_commit setting |
| config/horizon.php (if present) | Horizon environments, balance strategy, worker counts, timeouts |
| app/Jobs/*.php | Job classes, ShouldQueue usage, tries/backoff, failed() methods |
| app/Console/Kernel.php or routes/console.php | Scheduled jobs (Schedule::job(...), withoutOverlapping) |
| database/migrations/*_create_failed_jobs_table.php | Failed-jobs storage migration; or DynamoDB driver in config |
| supervisor*.conf / /etc/supervisor/conf.d/ | Worker process management, numprocs, --max-time, stopwaitsecs |
Typical setups:
| Stack | Queue driver | Failed driver | Worker manager |
|---|---|---|---|
| Small Laravel + MySQL | database | database | Supervisor (or systemd) |
| Production Laravel + Redis | redis | database (or dynamodb for serverless) | Supervisor + Horizon |
| Laravel Vapor | sqs | dynamodb | Vapor-managed |
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Driver & Config | CRITICAL | config- |
| 2 | Job Design | CRITICAL | design- |
| 3 | Retry & Failure | HIGH | retry- |
| 4 | Scaling & Workers | HIGH | scaling- |
| 5 | Batching & Chaining | HIGH | bus- |
| 6 | Testing & Operations | MEDIUM | ops- |
Quick Reference
1. Driver & Config (CRITICAL)
config-driver-choice—databasefor small apps;redisfor production scale;sqsfor Laravel Vaporconfig-after-commit— setafter_commit: trueto prevent dispatching jobs that reference uncommitted DB rowsconfig-failed-storage—failed_jobstable is required (or DynamoDB on Vapor); set up retention/cleanup
2. Job Design (CRITICAL)
design-shouldqueue— every async job MUST implementShouldQueue(the #1 production bug: forgetting it makes the job run synchronously)design-pass-ids-not-models— pass IDs to the constructor; refetch inhandle()— avoids stale models and bloated serialised payloadsdesign-idempotency— payment, external API, and "create resource" jobs must be safe to run twice (ShouldBeUnique, idempotency keys, unique DB constraints)design-constructor-vs-handle— constructor runs at dispatch (sync);handle()runs on the worker. No DB writes or HTTP calls in the constructor.
3. Retry & Failure (HIGH)
retry-tries-and-backoff— set both:#[Backoff([1, 5, 30])]for exponential delays; default 3 tries is usually too few for transient failuresretry-failed-method— implementfailed(Throwable $e)for permanent-failure handling (alert, refund, mark-as-failed)retry-transient-vs-permanent—release($delay)for transient errors (rate-limited, network); throw for permanentretry-fail-on-timeout—#[FailOnTimeout]to avoid burning all attempts on hung jobs
4. Scaling & Workers (HIGH)
scaling-supervisor-config— Supervisor (or systemd) manages workers;stopwaitsecs > timeout;--max-time=3600to recyclescaling-multi-queue-priority— high/default/low queue lanes for SLA-critical jobs;--queue=high,default,lowscaling-worker-recycling—--max-jobsand--max-timeto combat memory leaks in long-running workers
5. Batching & Chaining (HIGH)
bus-batch-vs-chain—Bus::batchfor parallel + progress tracking;Bus::chainfor strict sequentialbus-batch-failure-handling—allowFailures()for fault-tolerant batches; usethen/catch/finallycallbacksbus-chunking-large-sets— for 1000+ items, chunk viaBus::batch(...)rather than one job per item
6. Testing & Operations (MEDIUM)
ops-queue-fake—Queue::fake()/Bus::fake()in tests;assertDispatched,assertPushed,assertChained,assertBatchedops-schedule-queued-jobs—Schedule::job(new X)->everyMinute()queues the job; pair withwithoutOverlapping()for safetyops-horizon-when— adopt Horizon when on Redis with multiple supervisors; not fordatabasequeue or single-worker setups
Essential Patterns
Minimum-viable job class (Laravel 13)
<?php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\FailOnTimeout;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
#[Backoff([1, 5, 30])]
#[FailOnTimeout]
class ChargeOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public int $timeout = 30;
public function __construct(public readonly int $orderId) {}
public function handle(StripeGateway $stripe): void
{
$order = Order::findOrFail($this->orderId); // refetch — don't trust serialised state
if ($order->status === 'paid') return; // idempotency guard
$charge = $stripe->charge($order);
$order->markPaid($charge->id);
}
public function failed(Throwable $e): void
{
Order::find($this->orderId)?->markPaymentFailed($e->getMessage());
}
}
Dispatching
ChargeOrder::dispatch($order->id); // default queue
ChargeOrder::dispatch($order->id)->onQueue('high'); // priority lane
ChargeOrder::dispatch($order->id)->delay(now()->addMinutes(5));
ChargeOrder::dispatchAfterResponse($order->id); // run after HTTP response sent
Supervisor config (recommended)
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --queue=high,default,low --sleep=3 --tries=3 --max-time=3600 --backoff=3
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log
stopwaitsecs=3600
Critical: stopwaitsecs must be greater than your longest job's timeout, or Supervisor will kill mid-job on deploy.
References
- Laravel 13 — Queues
- Laravel 13 — Horizon
- Laravel 13 — Task Scheduling
- Supervisor — A Process Control System
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md