Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series. Pairs well with Laravel Queue Workers: How They Work.
A few queue settings trip up nearly everyone, mostly because two of them look like they do the same thing and actually do not. Here is each one in plain terms, with the trap that catches people spelled out.
retry_after vs timeout (the big one)
These two sound identical and are completely different.
timeout is a worker option. It is the maximum number of seconds a job is allowed to run before the worker forcibly kills it. It is enforced by the worker process itself.
php artisan queue:work --timeout=60
retry_after is a connection setting in config/queue.php. It is the number of seconds the queue waits before deciding a reserved job has died and making it available for another worker to pick up.
'redis' => [
'driver' => 'redis',
'retry_after' => 90,
// ...
],
The rule that ties them together: retry_after must always be larger than timeout. If retry_after is shorter, the queue will hand the job to a second worker while the first one is still legitimately running it, and you get the same job executing twice. A safe gap, say timeout 60 and retry_after 90, means the job is killed by its timeout well before the queue ever considers it lost.
block_for (Redis)
On the Redis connection, block_for tells the worker how long to block and wait for a job to appear, instead of polling, asking, sleeping, and asking again.
'redis' => [
'driver' => 'redis',
'block_for' => 5,
// ...
],
With block_for set, the worker holds the connection open and Redis hands it a job the instant one arrives, which lowers latency and reduces wasted polling. Leave it null if you run multiple queues on one worker, because blocking on one queue can starve the others.
Queue priority
List several queues on a worker and the order is the priority order. The worker fully drains the first queue before looking at the next.
php artisan queue:work --queue=high,default,low
Here every high job runs before any default job, and default before low. This is the simplest way to make sure urgent work jumps the line. The flip side is starvation: if high is never empty, low may never run, so reserve strict priority for work that genuinely must go first.
tries and backoff
tries is how many attempts a job gets before it is marked failed; backoff is how long to wait between attempts. Both can be worker options or job properties.
class SyncToCrm implements ShouldQueue
{
public $tries = 5;
public $backoff = [10, 30, 60]; // wait 10s, then 30s, then 60s
}
Passing an array to backoff gives you exponential style retries: a short wait first, longer waits after, which is ideal for flaky external services that may just need a moment.
retryUntil for time based expiry
Sometimes "try 5 times" is the wrong limit and "keep trying until this moment" is right. A retryUntil method lets a job expire by wall clock time rather than attempt count.
public function retryUntil(): \DateTime
{
return now()->addMinutes(10);
}
The job will be retried as often as needed until that time passes, then it stops. This is perfect for work that is only useful for a short window, like a notification that is pointless once it is stale.
stop-when-empty, and when not to use it
The --stop-when-empty option makes a worker exit as soon as the queue is empty rather than sleeping and waiting for more.
php artisan queue:work --stop-when-empty
It is handy for short lived containers or scheduled drain jobs that should run, clear the backlog, and quit. Do not use it for your normal always on workers, because a brief empty moment will shut them down and you will need something to keep restarting them. For long running workers you want them to stay alive and wait.
Wrapping up
Most queue confusion is really just retry_after versus timeout: one is the worker killing a slow job, the other is the queue reclaiming a lost one, and retry_after must be the larger of the two. Beyond that, block_for cuts Redis polling, queue order sets priority, tries and backoff shape retries, retryUntil expires jobs by time, and --stop-when-empty is for drain-and-quit workers only.
Next up: how Laravel prepares a job for the queue and how Laravel Horizon works. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.