Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series. Many of these arrived around Laravel 8 and are now standard, so if you learned queues on an older version, this is your catch up.
The Laravel queue system grew a set of features that, taken together, make it genuinely production grade. Here is a tour of the ones worth having in your toolkit, with the modern names and syntax.
Backoff, with exponential retries
The old retryAfter property and method are now backoff, and the worker's --delay option is now --backoff. The nice part: pass an array for exponential backoff.
public $backoff = [30, 60]; // wait 30s after the first failure, 60s after each later one
php artisan queue:work --backoff=30,60
retryUntil for time based expiry
The old timeoutAt is now retryUntil. Instead of capping by attempt count, you keep retrying until a moment in time.
public function retryUntil(): \DateTime
{
return now()->addDay();
}
This pairs perfectly with $tries = 0 and maxExceptions for self-releasing jobs, as covered in too many attempts.
Job chains
Chain jobs so each runs only after the previous one succeeds, with a catch for the whole chain.
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ExtractReports,
new GenerateReport,
new SendResults,
])->onConnection('redis')->onQueue('reports')->catch(function (Throwable $e) {
// a job in the chain failed
})->dispatch();
Job batches
Dispatch a group to run in parallel and react when they all finish or one fails. There is a whole post on batching, but the shape is:
Bus::batch([
new ProcessFile(1),
new ProcessFile(2),
new ProcessFile(3),
])->then(fn () => /* all done */)->dispatch();
Queued closures with catch
You can queue a closure directly and attach a failure handler.
dispatch(function () {
// job logic
})->catch(function (Throwable $e) {
// handle failure
});
Self terminating workers
To fight memory growth, workers can exit themselves after a number of jobs or a span of time, and your process manager restarts them. This replaced the old "cron a queue:restart" habit for many people.
php artisan queue:work --max-jobs=1000 --max-time=3600
There is more on this in avoiding memory leaks.
Named workers and custom queue selection
A --name option lets you label a worker, which you can use to control how it picks queues at run time, for example draining different queues at night versus during the day.
php artisan queue:work --name=notifications
Reliability touches under the hood
Two quieter improvements you benefit from without doing anything. The database driver now releases jobs inside a transaction, so a job is not removed from the queue unless its released copy is safely stored, which sharply reduces the chance of losing jobs. And the Redis driver dispatches a bulk group of jobs in a single command instead of one push per job, which is why batches and bulk dispatch are efficient. Workers also terminate gracefully and run any App::terminating() callbacks on the way out.
Horizon balancing controls
Horizon's auto balancing gained two knobs: balanceMaxShift (how many workers it may add or remove per cycle) and balanceCooldown (how long it waits between adjustments), so you can tune how aggressively it reacts to load.
'supervisor-1' => [
'balance' => 'auto',
'balanceMaxShift' => 5,
'balanceCooldown' => 3,
],
Wrapping up
Modern Laravel queues give you exponential backoff, time based retryUntil, Bus::chain and Bus::batch, queued closures with catch, self terminating workers, named workers, and finer Horizon balancing, on top of quiet reliability gains in the database and Redis drivers. If your mental model of Laravel queues predates these, this is the set to internalise.
Browse the rest of the series, starting with Laravel Queue Workers: How They Work. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.