Laravel 12.40's Queue Pause and Resume: Finally, Surgical Control Over Your Workers.
December 3, 2025
7 minutes
Right, let's talk about something that's might have been a pain for you over the years. You've got a queue processing thousands of jobs, everything's humming along nicely, and then... disaster. Your email provider's API starts throwing rate limit errors. Or your payment gateway is having a wobble. Or you need to do some urgent maintenance on a specific integration.
Before Laravel 12.40, your options were essentially: stop all the workers (and watch everything grind to a halt), or let the jobs fail and deal with the carnage later.
Neither of those are good options. We've all been there.
Well, thanks to a contribution from Yousef Kadah, Laravel 12.40 now includes the ability to pause and resume individual queues without touching your workers. And honestly, it's one of those features that makes you wonder why it took this long, it's not a huge feature but a useful one and one that we know in the future we will be using.
The Problem (And Why It's Actually Your Problem)
Here's a scenario that'll feel familiar. You've got a multi-queue setup:
1queue:work --queue=payments,emails,notifications
Your email provider (let's call them... Mailchimp? Postmark? Whatever) starts returning 429 errors. You're hitting their rate limit. Every job that processes is going to fail, retry, fail again, and eventually end up in your failed jobs table.
Before Laravel 12.40, your options were:
Stop all workers - But now payments and notifications are backing up too
Let it fail - Watch your retry attempts burn through and jobs pile up in
failed_jobsDeploy code changes - Add middleware to catch rate limits, deploy, pray
Custom hacks - Build your own pause system with cache flags and middleware
None of these are great. Option 1 causes collateral damage. Option 2 is chaos. Options 3 and 4 take time you don't have when things are on fire.
Enter Queue::pause() and Queue::resume()
Laravel 12.40 introduces a beautifully simple solution:
1<?php 2 3use Illuminate\Support\Facades\Queue; 4 5// Pause the emails queue on the redis connection 6Queue::pause(connection: 'redis', queue: 'emails'); 7 8// ... perform maintenance, wait for rate limits to reset ... 9 10// Resume processing11Queue::resume(connection: 'redis', queue: 'emails');12 13// Check if a queue is paused14Queue::isPaused(connection: 'redis', queue: 'emails'); // true/false
That's it. Your workers keep running. Payments keep processing. Notifications keep firing. The emails queue just... waits.
No jobs are lost. They sit there patiently until you resume. Your workers skip over the paused queue and continue processing everything else.
How It Actually Works (The Technical Bit)
Under the hood, this is elegantly simple. When you pause a queue, Laravel stores a flag in your cache:
1<?php2 3$this->app['cache']4 ->store()5 ->forever("illuminate:queue:paused:{$connection}:{$queue}", true);
The queue worker has been modified to check this flag before attempting to pop a job from any queue. If the queue is paused, the worker simply moves on to the next queue in its list.
Here's the clever bit: because it uses your cache driver, this works across all your workers instantly. Pause a queue from an admin panel, and every worker across every server knows about it immediately.
The Artisan Commands
Of course, there are Artisan commands for when you're SSH'd into production at 2am:
1# Pause a queue 2php artisan queue:pause redis:emails 3 4# Resume a queue (or use queue:continue - they're aliases) 5php artisan queue:resume redis:emails 6php artisan queue:continue redis:emails 7 8# If you don't specify a connection, it uses your default 9php artisan queue:pause emails10php artisan queue:resume emails
The syntax follows the same pattern as queue:monitor - connection:queue. Nice and consistent.
Timed Pauses with pauseFor()
Here's where it gets really useful. Laravel 12.40.2 added pauseFor() which lets you pause a queue for a specific duration:
1<?php2 3use Illuminate\Support\Facades\Queue;4 5// Pause the emails queue for 30 seconds6Queue::pauseFor(connection: 'redis', queue: 'emails', ttl: 30);7 8// Pause for 5 minutes9Queue::pauseFor(connection: 'redis', queue: 'emails', ttl: 300);
After the TTL expires, the cache key disappears, and processing automatically resumes. No manual intervention needed.
This is perfect for handling rate limits programmatically. When your API client catches a 429 response, you can pause the queue for exactly as long as the Retry-After header tells you to wait:
1<?php 2 3namespace App\Jobs; 4 5use App\Services\EmailService; 6use Illuminate\Contracts\Queue\ShouldQueue; 7use Illuminate\Foundation\Queue\Queueable; 8use Illuminate\Support\Facades\Queue; 9use Illuminate\Http\Client\RequestException;10 11class SendMarketingEmail implements ShouldQueue12{13 use Queueable;14 15 public function __construct(16 public int $userId,17 public string $campaignId,18 ) {}19 20 public function handle(EmailService $emailService): void21 {22 try {23 $emailService->send($this->userId, $this->campaignId);24 } catch (RequestException $e) {25 if ($e->response->status() === 429) {26 $retryAfter = $e->response->header('Retry-After', 60);27 28 // Pause the entire queue for the rate limit duration29 Queue::pauseFor(30 connection: $this->connection ?? config('queue.default'),31 queue: $this->queue ?? 'default',32 ttl: (int) $retryAfter33 );34 35 // Release this job to be picked up when queue resumes36 $this->release((int) $retryAfter);37 }38 39 throw $e;40 }41 }42}
Now when you hit a rate limit, the queue pauses itself, waits the appropriate amount of time, and automatically resumes. All without human intervention.
Real-World Scenarios Where This Could Save Your Bacon
Payment Gateway Maintenance
Your payment provider sends you a heads-up: "Scheduled maintenance window, 02:00-02:30 UTC." Rather than hoping no payments come through, or building elaborate skip logic:
1<?php 2 3// In your scheduler 4use Illuminate\Support\Facades\Schedule; 5use Illuminate\Support\Facades\Queue; 6 7Schedule::call(function () { 8 Queue::pause(connection: 'redis', queue: 'payments'); 9})->at('02:00');10 11Schedule::call(function () {12 Queue::resume(connection: 'redis', queue: 'payments');13})->at('02:30');
Jobs queue up during the maintenance window and process when it's over. Lovely.
Third-Party API Rate Limits
We touched on this above, but it's worth showing a more complete middleware approach:
1<?php 2 3namespace App\Jobs\Middleware; 4 5use Closure; 6use Illuminate\Support\Facades\Queue; 7use Illuminate\Http\Client\RequestException; 8 9class HandleApiRateLimits10{11 public function __construct(12 private string $queue = 'default',13 private string $connection = 'redis',14 ) {}15 16 public function handle(object $job, Closure $next): void17 {18 try {19 $next($job);20 } catch (RequestException $e) {21 if ($e->response->status() === 429) {22 $retryAfter = (int) $e->response->header('Retry-After', 60);23 24 Queue::pauseFor(25 connection: $this->connection,26 queue: $this->queue,27 ttl: $retryAfter28 );29 30 $job->release($retryAfter);31 32 return;33 }34 35 throw $e;36 }37 }38}
Apply this middleware to any job that hits rate-limited APIs, and you've got automatic, intelligent backoff at the queue level.
Database Migration During Deployments
Need to run a long migration that'll lock a table your jobs depend on?
1<?php2 3// In your deployment script or a custom Artisan command4Artisan::call('queue:pause', ['queue' => 'redis:orders']);5 6// Run your migrations7Artisan::call('migrate', ['--force' => true]);8 9Artisan::call('queue:resume', ['queue' => 'redis:orders']);
Building an Admin Panel Control
Want to give your ops team a big red "pause" button? Easy:
1<?php 2 3namespace App\Http\Controllers\Admin; 4 5use Illuminate\Http\Request; 6use Illuminate\Support\Facades\Queue; 7 8class QueueController extends Controller 9{10 public function pause(Request $request)11 {12 $request->validate([13 'connection' => 'required|string',14 'queue' => 'required|string',15 ]);16 17 Queue::pause(18 connection: $request->connection,19 queue: $request->queue20 );21 22 return response()->json([23 'message' => "Queue {$request->connection}:{$request->queue} paused",24 ]);25 }26 27 public function resume(Request $request)28 {29 $request->validate([30 'connection' => 'required|string',31 'queue' => 'required|string',32 ]);33 34 Queue::resume(35 connection: $request->connection,36 queue: $request->queue37 );38 39 return response()->json([40 'message' => "Queue {$request->connection}:{$request->queue} resumed",41 ]);42 }43 44 public function status(Request $request)45 {46 $queues = ['default', 'emails', 'payments', 'notifications'];47 $connection = $request->get('connection', config('queue.default'));48 49 $status = collect($queues)->mapWithKeys(fn ($queue) => [50 $queue => Queue::isPaused(connection: $connection, queue: $queue)51 ]);52 53 return response()->json(['queues' => $status]);54 }55}
The Reality Check
Now, let's be honest about the limitations and gotchas, because you know we don't sugarcoat things here at Jump24.
Cache Dependency
The pause state lives in your cache. If your cache is Redis and Redis goes down, your pause state disappears. Not the end of the world, but worth knowing. If you're using the array cache driver (you shouldn't be in production), pauses won't persist across requests.
Horizon Compatibility
If you're using Laravel Horizon, this works fine – Horizon workers respect the pause state just like regular workers. However, Horizon's dashboard won't show you which queues are paused (yet). You'll need to check programmatically or via the Artisan commands.
Multi-Server Considerations
Because the pause state is in the cache, all your workers need to share the same cache. If you've got separate Redis instances per server (unusual, but we've seen it), pausing won't propagate to all workers.
No Built-In Listing
There's no queue:pause:list command to see all currently paused queues. The original PR had this, but Taylor simplified the initial release. If you need this, you'll have to build it yourself by checking your known queues:
1<?php2 3$queues = ['default', 'emails', 'payments'];4$paused = collect($queues)5 ->filter(fn ($queue) => Queue::isPaused('redis', $queue))6 ->values();
The Implementation Deep Dive
For the curious, here's roughly what's happening in the framework. Yousef's implementation is elegantly simple – the QueueManager gained three new methods:
1<?php 2 3// In Illuminate\Queue\QueueManager 4 5 /** 6 * Pause a queue by its connection and name. 7 * 8 * @param string $connection 9 * @param string $queue10 * @return void11 */12 public function pause($connection, $queue)13 {14 $this->app['cache']15 ->store()16 ->forever("illuminate:queue:paused:{$connection}:{$queue}", true);17 }18 19 /**20 * Pause a queue by its connection and name for a given amount of time.21 *22 * @param string $connection23 * @param string $queue24 * @param \DateTimeInterface|\DateInterval|int $ttl25 * @return void26 */27 public function pauseFor($connection, $queue, $ttl)28 {29 $this->app['cache']30 ->store()31 ->put("illuminate:queue:paused:{$connection}:{$queue}", true, $ttl);32 }33 34 /**35 * Resume a paused queue by its connection and name.36 *37 * @param string $connection38 * @param string $queue39 * @return void40 */41 public function resume($connection, $queue)42 {43 $this->app['cache']44 ->store()45 ->forget("illuminate:queue:paused:{$connection}:{$queue}");46 }47 48 /**49 * Determine if a queue is paused.50 *51 * @param string $connection52 * @param string $queue53 * @return bool54 */55 public function isPaused($connection, $queue)56 {57 return (bool) $this->app['cache']58 ->store()59 ->get("illuminate:queue:paused:{$connection}:{$queue}", false);60 }
And the Worker class now checks isPaused() before attempting to pop a job from each queue. Simple, elegant, effective.
Migration Strategy
If you've built your own pause system (we've definitely done this on projects before Laravel 12.40), here's how to migrate:
Update to Laravel 12.40+ – obviously
Remove custom middleware/logic – anything that checks cache flags for pausing
Update any admin interfaces – point them at the new
Queue::pause()andQueue::resume()methodsTest thoroughly – make sure your workers respect the new pause state
If you're using Horizon with a custom pause solution, you might want to keep both running in parallel for a release or two, just to be safe.
Should You Use This?
Absolutely. If you're running any kind of production Laravel application with queues – and let's face it, that's most of them – this gives you operational flexibility you didn't have before.
The ability to surgically pause one queue while everything else keeps running is genuinely useful. Rate limits, maintenance windows, debugging issues in production – all of these become easier to handle.
It's a small feature in terms of lines of code, but it solves a real problem that we've been working around for years. That's Laravel at its best – community members like Yousef identifying pain points and contributing solutions that benefit everyone.
Get Started Now
Update to Laravel 12.40:
1composer update laravel/framework
Then start using Queue::pause() and Queue::resume() where it makes sense. Consider building an admin interface for your ops team. Set up automatic rate limit handling with pauseFor().
And next time your email provider's API starts throwing 429s at 3am, you can pause that queue with a single command and go back to sleep.
Looking to scale your development team?
Don’t let limited resources hold you back. Get in touch to discuss your needs and discover how our team augmentation services can supercharge your success.