Laravel Queues in Practice — Jobs, Retries, and Horizon
Author: Gabriel
Published: 3/6/2026
Laravel’s queue system lets you defer time-consuming tasks — emails, notifications, API calls — to the background, keeping your app fast and responsive. In this guide we cover the full picture: drivers, job classes, retries, failed jobs, and monitoring with Horizon.
Every web app eventually hits the same wall: some tasks just take too long to run during a request. Sending emails, generating PDFs, calling third-party APIs, resizing images — these operations can add seconds to your response time and frustrate users.
Laravel’s queue system solves this elegantly. You push jobs to a queue, a worker picks them up in the background, and your HTTP response goes out immediately. Here’s how to use it properly.
Choosing a Queue Driver
Laravel supports several queue drivers out of the box. Set yours in .env:
QUEUE_CONNECTION=redis- sync — Runs jobs immediately, inline. Good for local development only.
- database — Stores jobs in a DB table. Simple to set up, no extra infrastructure.
- redis — Fast, reliable, recommended for production. Pairs perfectly with Laravel Horizon.
- sqs — Amazon SQS. Great for high-throughput apps on AWS.
For most production apps, Redis is the right choice.
Creating Your First Job
php artisan make:job SendWelcomeEmailThis generates a job class at app/Jobs/SendWelcomeEmail.php:
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
}The ShouldQueue interface is what tells Laravel to push this to the queue instead of running it immediately.
Dispatching Jobs
// Dispatch immediately to the queue
SendWelcomeEmail::dispatch($user);
// Delay execution by 5 minutes
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
// Dispatch to a specific queue
SendWelcomeEmail::dispatch($user)->onQueue('emails');
// Dispatch only if a condition is true
SendWelcomeEmail::dispatchIf($user->isNew(), $user);Retries and Timeouts
Network failures happen. Configure how Laravel handles them in your job class:
class SendWelcomeEmail implements ShouldQueue
{
public int $tries = 3; // Retry up to 3 times
public int $timeout = 60; // Kill the job after 60 seconds
public int $backoff = 30; // Wait 30s between retries
public function failed(Throwable $exception): void
{
// Notify your team, log to Sentry, etc.
Log::error('SendWelcomeEmail failed', ['error' => $exception->getMessage()]);
}
}Handling Failed Jobs
When a job exhausts its retries, Laravel stores it in the failed_jobs table. Set it up with:
php artisan queue:failed-table
php artisan migrateUseful Artisan commands for managing failed jobs:
# List all failed jobs
php artisan queue:failed
# Retry a specific failed job
php artisan queue:retry {id}
# Retry all failed jobs
php artisan queue:retry all
# Delete a failed job
php artisan queue:forget {id}Running Queue Workers
# Start a worker
php artisan queue:work
# Process a specific queue
php artisan queue:work --queue=emails,default
# Restart workers after each job (good for development)
php artisan queue:listenIn production, always run workers under a process manager like Supervisor to auto-restart them if they crash:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/worker.logMonitoring with Laravel Horizon
If you’re using Redis, install Horizon for a beautiful real-time dashboard:
composer require laravel/horizon
php artisan horizon:install
php artisan migrateThen run:
php artisan horizonHorizon gives you throughput metrics, failed job tracking, job tagging, and the ability to pause/resume queues — all from /horizon in your app.
Quick Tips
- Use separate queues — Don’t dump everything into one queue. Separate critical jobs (payments) from low-priority ones (emails).
- Keep jobs small — Pass IDs, not full Eloquent models when possible. Let the job re-fetch fresh data.
- Always implement
failed()— Silent failures are the worst kind. Log them or send alerts. - Call
php artisan queue:restartafter deploys — Workers cache the app, so old code keeps running until they restart.
Queues are one of those Laravel features that feel optional until the day you need them — and then you can’t imagine building without them.
Tags:
LaravelPHPQueues