Laravel queue design
Design Laravel queue jobs that survive production - idempotency, retries and backoff, failure handling, batching, chaining, and Horizon tuning. Use when the user creates or reviews queued jobs, debugs stuck or duplicated jobs, asks about retries or failed_jobs, or moves slow work out of the request cycle.From its SKILL.md
npx -y skills add kwhorne/elyra-skills --skill laravel-queue-designAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
5.0 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
Laravel Queue Design
A queued job will run twice, run late, or run after the world changed under it. Design every job for those three facts and production gets boring — in the good way.
When to use
- Creating or reviewing queued jobs, listeners, or notifications
- "Jobs are stuck / duplicated / silently failing"
- Moving slow work (mail, exports, API calls) out of the request
- Setting up or tuning Horizon / queue workers
Principles
- At-least-once means duplicates. Every handler must tolerate running twice — idempotency is the entry fee, not a nice-to-have.
- Small payload, fresh state. Pass IDs, re-fetch inside the job. The world at dispatch time is not the world at run time.
- Failure is a feature. Decide per job what retry, backoff, and final failure mean — the defaults are guesses.
- One job, one outcome. A job that does five things fails five ways and retries all of them.
Process
1. Design the job
class ProcessRefund implements ShouldQueue
{
public function __construct(public int $orderId) {} // ID, not model state you'll trust later
public $tries = 3;
public $backoff = [10, 60, 300]; // seconds; spread retries out
public $timeout = 120; // must be < retry_after in queue config!
public function handle(): void
{
$order = Order::findOrFail($this->orderId); // fresh state
if ($order->refunded) return; // idempotency check
// ...
}
}
- Eloquent models in constructors are fine (
SerializesModelsre-fetches), but decisions made on dispatch-time state are stale — re-check conditions inhandle() $timeout<retry_after(queue config) or the job runs twice concurrently — the classic Laravel queue foot-gun
2. Make it idempotent
Pick the mechanism that fits:
- State check:
if ($order->refunded) return;— re-read, then act - Unique jobs:
ShouldBeUniqueto prevent duplicate dispatch - DB constraint: unique index +
firstOrCreate/upsert absorbs the duplicate - External calls: pass an idempotency key to the payment/API provider
3. Decide the failure story
failed(Throwable $e)method: notify, mark the domain object, compensate- Don't retry the unretryable: validation-type failures →
$this->fail($e)immediately - Poison-pill protection:
$tries/$maxExceptionsso one bad payload doesn't loop forever - Monitor
failed_jobs— a table nobody looks at is a black hole. Alert on growth;queue:retryafter fixing the cause
4. Compose bigger work
| Need | Tool |
|---|---|
| Steps in order, abort on failure | Job chain (Bus::chain) |
| Fan-out N items + completion hook | Batch (Bus::batch(...)->then(...)->catch(...)) |
| Per-tenant/user serialization | WithoutOverlapping middleware |
| External API rate limits | RateLimited middleware, dedicated queue |
For batches: items must be independently retryable; allowFailures() if one bad item shouldn't sink the batch.
5. Separate queues by SLA
high(user-facing: mail verification, receipts),default,low(exports, sync)- Workers:
--queue=high,default,lowpriority order, or dedicated workers per queue - One slow queue must not starve fast ones — that's the whole point of separation
6. Operate it
- Horizon: balance/processes per queue, alert on wait time, watch failed rate
- Deploys:
queue:restart(workers hold old code in memory until restarted) - Track queue wait time (latency from dispatch to start) — it's the user-facing number
Output format
## Queue review: <job/feature>
| Job | Idempotent via | tries/backoff | timeout<retry_after | On final failure |
|-----|----------------|---------------|---------------------|------------------|
| … | state check | 3 / 10-60-300 | ✅ 120<300 | notify + mark |
### Gaps (ranked)
1. <job>: <risk> → <fix>
### Queue topology
high: …, default: …, low: … — workers: …
Anti-patterns
- ❌ Serializing a model and trusting its attributes at run time (stale decisions)
- ❌
$timeout≥retry_after— concurrent double execution by design - ❌ Default retries on a non-idempotent job touching money
- ❌ One giant job processing 10 000 rows instead of a batch of small ones
- ❌
failed_jobsas a write-only table with no alerting - ❌ Forgetting
queue:restarton deploy, then debugging "old code" for an hour - ❌ Everything on the
defaultqueue — exports starving password-reset mails
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.