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 SendWelcomeEmail

This 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 migrate

Useful 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:listen

In 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.log

Monitoring 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 migrate

Then run:

php artisan horizon

Horizon 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:restart after 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

More Posts