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.