2026-03-28 15:07:29 -05:00
# 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 ;
2026-05-14 08:59:57 -05:00
public function retryUntil () : \DateTimeInterface
2026-03-28 15:07:29 -05:00
{
return now () -> addHours ( 4 );
}
```
2026-05-14 08:59:57 -05:00
## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
2026-03-28 15:07:29 -05:00
2026-05-14 08:59:57 -05:00
`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
2026-03-28 15:07:29 -05:00
```php
2026-05-14 08:59:57 -05:00
class UpdateSearchIndex implements ShouldQueue , ShouldBeUniqueUntilProcessing
2026-03-28 15:07:29 -05:00
{
2026-05-14 08:59:57 -05:00
// Lock releases when processing begins, not when it finishes
2026-03-28 15:07:29 -05:00
}
```
## 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 ,
],
],
],
```