Laravel's Failover Queue Driver: How to Never Lose a Job
If you've ever had Redis go down during a deployment and watched jobs vanish into thin air, you know the feeling. Your users don't get their confirmation emails, webhooks don't fire, and you're left scrambling to figure out what was lost.
Since Laravel 12.34, the framework ships with a failover queue driver that handles this automatically. When your primary queue connection fails, Laravel tries the next one in your list. No code changes in your job classes, no custom retry logic. Just a config update and your jobs have a safety net.
In this post I'll walk you through how it works under the hood, how to configure it, how to monitor failover events, and some practical patterns for different application sizes.
How It Works
The failover driver wraps multiple queue connections into a single connection. When you dispatch a job, Laravel tries to push it to the first connection in your list. If that connection throws an exception (connection timeout, authentication failure, network issue), Laravel catches it and immediately tries the next connection.
This continues down the list until either the job is successfully pushed or all connections have failed. If every connection fails, Laravel throws a RuntimeException.
The important thing to understand is that failover only applies when pushing jobs. It doesn't affect how jobs are consumed. The pop() method (used by your queue workers) always reads from the first connection. This means you need to run separate workers for each connection in your failover list so that jobs landing on backup connections still get processed.
Configuration
Setting this up takes about 30 seconds. Open your config/queue.php file and add a failover connection alongside your existing connections:
1'connections' => [ 2 3 'redis' => [ 4 'driver' => 'redis', 5 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 6 'queue' => env('REDIS_QUEUE', 'default'), 7 'retry_after' => 90, 8 'block_for' => null, 9 'after_commit' => false,10 ],11 12 'database' => [13 'driver' => 'database',14 'connection' => env('DB_QUEUE_CONNECTION'),15 'table' => env('DB_QUEUE_TABLE', 'jobs'),16 'queue' => env('DB_QUEUE', 'default'),17 'retry_after' => 90,18 'after_commit' => false,19 ],20 21 'failover' => [22 'driver' => 'failover',23 'connections' => [24 'redis',25 'database',26 ],27 ],28 29],
Then update your .env to use the failover connection as your default:
1QUEUE_CONNECTION=failover
That's it. Every job your application dispatches will now try Redis first, and if Redis is unavailable, the job lands in your database queue instead.
Running Workers
Since failover only affects the push side, you need workers listening on each connection. If Redis goes down and jobs start landing in the database queue, those jobs need a worker to process them.
1php artisan queue:work redis2php artisan queue:work database
There are a few drivers where you don't need a separate worker:
syncprocesses jobs immediately in the current processbackgroundspawns a separate PHP process for each jobdeferredprocesses jobs after the HTTP response is sent
If you're using any of those as a fallback, you can skip the worker for that connection.
Supervisor Configuration
In production, you'll want Supervisor managing your workers. Here's a configuration that covers both connections:
1[program:queue-redis] 2process_name=%(program_name)s_%(process_num)02d 3command=php /home/forge/app.com/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 4autostart=true 5autorestart=true 6stopasgroup=true 7killasgroup=true 8user=forge 9numprocs=410redirect_stderr=true11stdout_logfile=/home/forge/app.com/storage/logs/worker-redis.log12stopwaitsecs=360013 14[program:queue-database]15process_name=%(program_name)s_%(process_num)02d16command=php /home/forge/app.com/artisan queue:work database --sleep=3 --tries=3 --max-time=360017autostart=true18autorestart=true19stopasgroup=true20killasgroup=true21user=forge22numprocs=223redirect_stderr=true24stdout_logfile=/home/forge/app.com/storage/logs/worker-database.log25stopwaitsecs=3600
I'd recommend running fewer processes for the database worker since it should only be handling overflow during outages, not your steady-state load.
If you're using Laravel Forge, you don't need to write Supervisor config files by hand. Forge lets you add workers through its UI under the "Queue" tab of your server. Just add one worker for each connection (e.g. one for redis, one for database) and Forge handles the Supervisor configuration for you.
If you're using Laravel Horizon to manage your Redis queues, Horizon replaces the Redis Supervisor worker entirely. You'd run Horizon for Redis and only need a separate Supervisor worker for the database fallback. More on Horizon and failover further down in this post.
Monitoring Failover Events
When a connection fails and Laravel switches to the next one, it dispatches a QueueFailedOver event. You should listen for this because a failover means something is wrong with your primary connection, and you probably want to know about it.
The event gives you three properties:
connectionName- the connection that just failed (e.g.redis)command- the job instance that was being pushedexception- the exception that caused the failure
Here's how to set up a listener in your AppServiceProvider:
1<?php 2 3namespace App\Providers; 4 5use Illuminate\Queue\Events\QueueFailedOver; 6use Illuminate\Support\Facades\Event; 7use Illuminate\Support\Facades\Log; 8use Illuminate\Support\ServiceProvider; 9 10class AppServiceProvider extends ServiceProvider11{12 public function boot(): void13 {14 Event::listen(QueueFailedOver::class, function (QueueFailedOver $event) {15 Log::warning('Queue failover activated', [16 'failed_connection' => $event->connectionName,17 'job' => is_object($event->command)18 ? get_class($event->command)19 : $event->command,20 'error' => $event->exception->getMessage(),21 ]);22 });23 }24}
In a production application, you'd probably want to send a Slack or email notification here too. If failover is happening, someone should be looking at why Redis (or whatever your primary is) went down.
Your Jobs Don't Need to Change
One of the best things about this feature is that your job classes stay exactly the same. A job like this works with the failover driver without any modifications:
1<?php 2 3namespace App\Jobs; 4 5use App\Models\Order; 6use App\Notifications\OrderShipped; 7use Illuminate\Contracts\Queue\ShouldQueue; 8use Illuminate\Foundation\Queue\Queueable; 9 10class NotifyCustomerOfShipment implements ShouldQueue11{12 use Queueable;13 14 public function __construct(15 public Order $order16 ) {}17 18 public function handle(): void19 {20 $this->order->customer->notify(21 new OrderShipped($this->order)22 );23 }24}
When you dispatch this job, the failover driver handles everything at the connection level. Your job doesn't know or care which connection it ended up on.
1NotifyCustomerOfShipment::dispatch($order);
If Redis is up, the job goes to Redis. If Redis is down, it goes to the database. Either way, a worker picks it up and your customer gets their notification.
Practical Patterns
Redis + Database (Most Applications)
This is the pattern I'd recommend for most applications. Redis is fast and handles the majority of your load. The database is always available (if your database is down, you have bigger problems) and acts as a reliable backup.
1'failover' => [2 'driver' => 'failover',3 'connections' => ['redis', 'database'],4],
Redis + SQS + Database (Larger Applications)
For larger applications where you want a managed service as your secondary, you can chain multiple connections. SQS gives you durability without putting extra load on your database, and the database acts as a last resort.
1'failover' => [2 'driver' => 'failover',3 'connections' => ['redis', 'sqs', 'database'],4],
Remember that each connection needs its own worker, so with this pattern you're running three sets of workers.
Redis + Sync (Small Applications or Local Development)
For small applications or local development where losing a job isn't critical but you still want things to work, sync processes the job immediately in the current request. No worker needed.
1'failover' => [2 'driver' => 'failover',3 'connections' => ['redis', 'sync'],4],
The trade-off here is that if Redis fails, your HTTP response will be slower since the job runs synchronously. For a quick email dispatch or a lightweight notification, that's often fine.
Horizon Considerations
If you use Laravel Horizon to manage your Redis queues, keep in mind that Horizon only knows about Redis. When failover routes a job to a database or sqs connection, Horizon won't display it in the dashboard and won't track its metrics.
This isn't a bug, it's just how Horizon works. You need a standard php artisan queue:work database process running alongside Horizon to handle any jobs that land in the fallback queue.
Things to Keep in Mind
Jobs should be idempotent. There's a small window where a job could potentially be pushed to a connection that then fails before the worker processes it, and the failover retries on the next connection. Design your jobs so that running them twice doesn't cause problems.
All connections failing throws an exception. If every connection in your failover list is down, Laravel throws a RuntimeException with the message "All failover queue connections failed." Your application should handle this gracefully, whether that means catching it in a try/catch or letting your exception handler deal with it.
pushRaw() doesn't fire failover events. If you're using pushRaw() to push raw payloads onto the queue, the QueueFailedOver event won't be dispatched during failover. The failover itself still works, but you won't get the event notification.
Wrapping Up
The failover queue driver is one of those features where the value is huge and the effort to set it up is minimal. You change a config file, make sure you're running workers for each connection, and you've got a safety net for your entire job pipeline.
For most Laravel applications, a simple Redis + Database failover setup is all you need. Your jobs keep working, your users don't notice a thing, and you get an event telling you something needs attention. That's a solid trade for a few lines of configuration.
Topics
Syntax highlighting by Torchlight
More articles
How to Fix "SQLSTATE[42S22]: Column Not Found" Error in Laravel 12 (2026 Guide)
The "Column not found: 1054 Unknown column" error is one of the most common database issues in Laravel. Learn how to diagnose and fix it, whether it's a typo, a missing migration, or an Eloquent relationship issue.
Read articleHow to Fix "Vite Manifest Not Found" Error in Laravel 12 (2026 Guide)
Getting the "Vite manifest not found" or "Mix manifest does not exist" error in Laravel? This guide covers all the common causes, from missing builds to deployment issues, with step-by-step fixes for Laravel 12 and 13.
Read article