The Deploy Race Condition Every Laravel Developer Knows
There is a brief window during most deploys where new code is on disk but queue workers have not restarted yet. A worker that picks up a job in that window deserializes a payload written by the old code and runs it through the new code. Usually nothing breaks. Occasionally a renamed job class or a changed constructor signature produces a failed job you have to replay by hand.
The traditional fixes each have drawbacks:
- Maintenance mode stops HTTP traffic too, which is often more than you want.
queue:restartis a polite request, not a guarantee — a worker only sees it between jobs.
Laravel 13.25 adds a third option: a global pause switch that stops every worker on every connection from reserving new work, while leaving HTTP traffic completely unaffected.
The Commands
# Pause all queues before deploying
php artisan queue:pause --all
# Resume after the deploy is complete
php artisan queue:resume --all
The queue argument is now optional on both commands. Without --all they behave as before and accept a connection:queue pair.
The same functionality is available on the Queue facade for deploy scripts written in PHP or for an admin panel controller:
use Illuminate\Support\Facades\Queue;
Queue::pauseAll();
// run your deploy steps here
Queue::resumeAll();
What Pausing Actually Does
Pausing stops workers from reserving new jobs. The worker process stays alive and keeps looping — it just sleeps instead of popping from the queue. A job already being processed when you pause runs to completion, so queue:pause --all will not interrupt anything mid-flight.
Producers are unaffected: SomeJob::dispatch() continues writing to Redis or the database, and those jobs wait there until you resume.
Under the hood, the feature writes a single cache key — illuminate:queues:paused — using forever(). Workers already read the cache once per loop to check for restart and per-queue pause signals, and the global key is fetched in the same many() call, so there are no extra round trips.
Individual and Global Pauses Are Independent
pause() and pauseAll() write different cache keys and are deliberately unaware of each other. If a queue was paused individually before the deploy, resumeAll() leaves it paused:
Queue::pause('redis', 'imports'); // parked earlier to investigate a bad job
Queue::pauseAll(); // deploy starts
Queue::resumeAll(); // deploy finishes
Queue::isPaused('redis', 'imports'); // still true
This is exactly the behavior you want. Someone parked the imports queue on purpose an hour ago, and a deploy running resumeAll() should not silently undo that. Clearing an individual pause still requires queue:resume redis:imports.
Events
Two new events fire alongside the existing per-queue QueuePaused and QueueResumed:
use Illuminate\Queue\Events\QueuesPaused;
use Illuminate\Queue\Events\QueuesResumed;
Event::listen(function (QueuesPaused $event) {
Log::warning('All queues paused — deploy in progress');
});
These are useful for alerting, audit logs, or triggering external monitoring integrations.
Key Takeaways
php artisan queue:pause --allandQueue::pauseAll()are new in Laravel 13.25.- Workers stay alive but stop reserving jobs; in-flight jobs finish normally.
- Producers keep dispatching; jobs accumulate and are processed after
resumeAll(). - Global and per-queue pauses are independent —
resumeAll()does not clear individual pauses. - The feature is implemented as a single cache key with no extra round trips per worker loop.
- Contributed by Jack Bayliss in PR #61126.
Source: Pause All Laravel Queues During a Deploy — Laravel News