# Queue & Job Best Practices ## Set `retry_after` Greater Than `timeout` If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution. Incorrect (`retry_after` ≤ `timeout`): ```php class ProcessReport implements ShouldQueue { public $timeout = 120; } // config/queue.php — retry_after: 90 ← job retried while still running! ``` Correct (`retry_after` > `timeout`): ```php class ProcessReport implements ShouldQueue { public $timeout = 120; } // config/queue.php — retry_after: 180 ← safely longer than any job timeout ``` ## Use Exponential Backoff Use progressively longer delays between retries to avoid hammering failing services. Incorrect (fixed retry interval): ```php class SyncWithStripe implements ShouldQueue { public $tries = 3; // Default: retries immediately, overwhelming the API } ``` Correct (exponential backoff): ```php class SyncWithStripe implements ShouldQueue { public $tries = 3; public $backoff = [1, 5, 10]; } ``` ## Implement `ShouldBeUnique` Prevent duplicate job processing. ```php class GenerateInvoice implements ShouldQueue, ShouldBeUnique { public function uniqueId(): string { return $this->order->id; } public $uniqueFor = 3600; } ``` ## Always Implement `failed()` Handle errors explicitly — don't rely on silent failure. ```php public function failed(?Throwable $exception): void { $this->podcast->update(['status' => 'failed']); Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); } ``` ## Rate Limit External API Calls in Jobs Use `RateLimited` middleware to throttle jobs calling third-party APIs. ```php public function middleware(): array { return [new RateLimited('external-api')]; } ``` ## Batch Related Jobs Use `Bus::batch()` when jobs should succeed or fail together. ```php Bus::batch([ new ImportCsvChunk($chunk1), new ImportCsvChunk($chunk2), ]) ->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) ->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed')) ->dispatch(); ``` ## `retryUntil()` Needs `$tries = 0` When using time-based retry limits, set `$tries = 0` to avoid premature failure. ```php public $tries = 0; public function retryUntil(): \DateTimeInterface { return now()->addHours(4); } ``` ## Use `WithoutOverlapping::untilProcessing()` Prevents concurrent execution while allowing new instances to queue. ```php public function middleware(): array { return [new WithoutOverlapping($this->product->id)->untilProcessing()]; } ``` Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts. ## Use Horizon for Complex Queue Scenarios Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities. ```php // config/horizon.php 'environments' => [ 'production' => [ 'supervisor-1' => [ 'connection' => 'redis', 'queue' => ['high', 'default', 'low'], 'balance' => 'auto', 'minProcesses' => 1, 'maxProcesses' => 10, 'tries' => 3, ], ], ], ```