Back to blog
Laravel
Intermediate

Mastering Laravel Queues: A Deep Dive into Jobs, Workers, and Scaling

Laravel queues power asynchronous processing in modern web apps. This comprehensive guide covers core concepts, architecture, step‑by‑step implementation, real‑world examples, best practices, and scaling strategies.

July 12, 202620 min read

Introduction

In today's web landscape, user experience is often dictated not by the features you expose, but by how quickly those features respond. Long‑running tasks—such as sending emails, generating PDFs, processing images, or syncing data with external services—can cripple request latency if performed synchronously. Laravel solves this problem with a robust queue system that lets you defer work to background processes called jobs. This article takes you from the fundamentals of Laravel queues to advanced scaling techniques using Laravel Horizon and multiple worker processes.

Table of Contents

Core Concepts

Laravel's queue subsystem revolves around three primary abstractions:

  • Jobs – Plain PHP classes that encapsulate a single unit of work. Jobs implement the Illuminate\Contracts\Queue\ShouldQueue interface, which tells the framework to push the job onto a queue instead of executing it immediately.
  • Queues – Logical named containers where jobs are stored until a worker picks them up. Laravel supports many drivers (database, Redis, Amazon SQS, Beanstalkd, etc.), each offering different trade‑offs for durability, speed, and cost.
  • Workers – Long‑running CLI processes started with php artisan queue:work (or queue:listen) that continuously poll a queue, fetch jobs, and run their handle() method.

When you dispatch a job, Laravel serializes the job class, pushes the payload onto the chosen queue driver, and returns immediately to the HTTP request cycle. One or more workers then deserialize the payload, execute the job, and delete the payload on success or release it back onto the queue on failure.

Architecture Overview

The diagram below illustrates a typical Laravel queue architecture for a production environment.

+-------------------+        +-------------------+        +-------------------+|   Web Server (Nginx)  | --> |  Laravel App (PHP‑FPM) | --> |  Redis (Queue)   |+-------------------+        +-------------------+        +-------------------+                                    |                                                                     |   php artisan horizon                                              v                                                          +---------------------------+                                        |  Horizon Dashboard (Web)  |                         +---------------------------+                                    |                                    v                         +---------------------------+                         |  Queue Workers (Supervisor) |                         +---------------------------+

Key points:

  • The web server handles incoming HTTP requests and quickly dispatches jobs to Redis.
  • Redis acts as an in‑memory, persistent queue store. It offers sub‑millisecond latency and native data structures for reliable job tracking.
  • Supervisor (or systemd) keeps a pool of php artisan queue:work redis processes alive, automatically restarting them on crash.
  • Laravel Horizon provides a beautiful UI, metrics, and auto‑scaling capabilities for Redis queues.

Step‑By‑Step Guide

Below is a practical guide for setting up Laravel queues with Redis, creating a job, and running workers under Supervisor.

  1. Install Redis and the PHP extension
    sudo apt-get update && sudo apt-get install -y redis-serverpecl install redis && echo "extension=redis.so" | sudo tee /etc/php/8.2/cli/conf.d/20-redis.ini
    Verify Redis is running: redis-cli ping should return PONG.
  2. Configure Laravel

    In .env set the queue driver:

    QUEUE_CONNECTION=redisREDIS_HOST=127.0.0.1REDIS_PASSWORD=nullREDIS_PORT=6379
    Run php artisan config:cache to refresh config.
  3. Generate a job class
    php artisan make:job SendWelcomeEmail --queued
    The --queued flag automatically adds the ShouldQueue interface.
  4. Implement the job logic
    userId = $userId;    }    public function handle()    {        $user = \App\Models\User::findOrFail($this->userId);        Mail::to($user->email)->send(new WelcomeMail($user));    }}?>
  5. Dispatch the job from a controller
    use App\Jobs\SendWelcomeEmail;public function register(Request $request){    $user = User::create($request->only(['name','email','password']));    SendWelcomeEmail::dispatch($user->id);    return response()->json(['message' => 'User created, email queued.']);}
  6. Start a worker
    php artisan queue:work redis --sleep=3 --tries=3
    For production, you will want a process monitor. Below we configure Supervisor.
  7. Configure Supervisor
    [program:laravel-queue]process_name=%(program_name)s_%(process_num)02dcommand=php /var/www/laravel/artisan queue:work redis --sleep=3 --tries=3 --daemonautostart=trueautorestart=trueuser=www-datanumprocs=4redirect_stderr=truestdout_logfile=/var/log/laravel/queue.log
    Run sudo supervisorctl reread && sudo supervisorctl update && sudo supervisorctl start laravel-queue:*.
  8. Install Horizon (optional but recommended)
    composer require laravel/horizonphp artisan horizon:installphp artisan migratephp artisan horizon
    Horizon will now monitor your Redis queues, offering retry dashboards and auto‑scaling.

Real‑World Examples

Below are three scenarios where queues dramatically improve reliability.

  • Bulk Email Campaigns – Instead of looping over 10,000 recipients in a single request, dispatch a SendCampaignEmail job for each recipient. Workers parallelize the work, and failures are isolated per‑email.
  • Image & Video Processing – When a user uploads media, store the raw file, then queue a ProcessMedia job that runs FFmpeg or ImageMagick in the background. This keeps the upload endpoint snappy.
  • Third‑Party API Synchronization – Rate‑limited APIs (e.g., Stripe, Twilio) can be wrapped in queued jobs with exponential back‑off, preventing HTTP timeout errors.

Production Code Examples

These snippets demonstrate idiomatic patterns you will encounter in production codebases.

/** * Job that uploads a video to AWS S3 and creates a thumbnail. */class ProcessVideo implements ShouldQueue{    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;    public $videoPath;    public $userId;    protected $tries = 5;    protected $backoff = [60, 120, 300]; // seconds between retries    public function __construct(string $videoPath, int $userId)    {        $this->videoPath = $videoPath;        $this->userId = $userId;    }    public function handle()    {        // 1. Upload original video        $s3 = \Storage::disk('s3');        $s3Path = "videos/{$this->userId}/" . basename($this->videoPath);        $s3->put($s3Path, file_get_contents($this->videoPath));        // 2. Generate thumbnail using FFmpeg (executed via Symfony Process)        $process = new \Symfony\Component\Process\Process([            'ffmpeg', '-i', $this->videoPath, '-ss', '00:00:01.000', '-vframes', '1', '-f', 'image2', '-'        ]);        $process->mustRun();        $thumbnail = $process->getOutput();        $thumbPath = "thumbnails/{$this->userId}/" . pathinfo($this->videoPath, PATHINFO_FILENAME) . '.jpg';        $s3->put($thumbPath, $thumbnail);        // 3. Update DB record        \App\Models\Video::where('path', $this->videoPath)            ->update(['s3_path' => $s3Path, 'thumbnail_path' => $thumbPath]);    }}

Comparison Table

Feature Database Driver Redis Driver SQS Driver
Persistence Durable (SQL transaction) In‑memory with optional RDB snapshot Fully durable (AWS)
Latency ~10‑20 ms ~1‑2 ms ~30‑50 ms (network)
Scalability Limited by DB I/O Horizontal scaling via clustering Unlimited via SQS queues
Visibility Timeout Supported Supported Supported (default 30 s)
Dashboard None (custom) Laravel Horizon AWS Console

Best Practices

  • Keep jobs small – A job should do one logical unit of work. Split large tasks into multiple jobs using dispatchAfterResponse or job chaining.
  • Idempotency – Ensure the handle() method can be safely retried without side‑effects. Use database unique constraints where possible.
  • Use rate‑limited queues – Laravel's RateLimited middleware can throttle jobs to respect external API limits.
  • Monitor with Horizon – Set up alerts for failed jobs, long‑running jobs, and queue depth.
  • Graceful shutdown – Send SIGTERM to workers and let them finish the current job before exiting. Horizon handles this automatically.

Common Mistakes

  • Dispatching jobs without implementing ShouldQueue, causing them to run synchronously.
  • Running queue:listen in production; it boots the entire framework on each loop, leading to memory leaks.
  • Storing large blobs (e.g., entire file contents) in the job payload. Use storage paths instead.
  • Neglecting tries and timeout values, which can cause hung workers.
  • Not configuring supervisor correctly, resulting in workers being killed after a short idle period.

Performance Tips

  • Prefer Redis over the database driver for high‑throughput queues.
  • Enable php artisan queue:work --daemon (default) to avoid booting the framework on every job.
  • Set --memory=128 to recycle workers after they exceed a memory threshold.
  • Leverage Horizon's auto‑scaling: configure maxProcesses and minProcesses based on queue length.
  • Batch jobs using dispatchBatch to get a single job identifier for monitoring.

Security Considerations

  • Never trust data deserialized from the queue without validation. Laravel automatically encrypts the payload, but business‑level validation is still required.
  • Limit the queue connection credentials in .env and rotate them periodically.
  • Use Laravel's Queue::after and Queue::failing events to log audit trails of job execution.
  • When using third‑party services, store API keys in Laravel's secret vault (e.g., php artisan secret:store) and inject them at runtime.

Deployment Notes

Deploying a queue‑enabled Laravel application typically involves three steps:

  1. Provision a Redis instance (managed or self‑hosted). Ensure persistence (RDB + AOF) is enabled for durability.
  2. Deploy the Laravel codebase (zero‑downtime tools like Envoy or Laravel Forge).
  3. Start and monitor workers with Supervisor or systemd. Example systemd unit:
[Unit]Description=Laravel Queue WorkerAfter=network.target[Service]User=www-dataGroup=www-dataRestart=alwaysExecStart=/usr/bin/php /var/www/laravel/artisan queue:work redis --sleep=3 --tries=3 --timeout=90[Install]WantedBy=multi-user.target

Enable the unit with sudo systemctl enable laravel-queue && sudo systemctl start laravel-queue. For high traffic, scale out the number of ExecStart processes or use Horizon's php artisan horizon which already spawns multiple workers under a single process tree.

Debugging Tips

  • Failed job table – Run php artisan queue:failed-table and php artisan migrate. Then inspect failed_jobs for exception messages.
  • Horizon metrics – Use php artisan horizon:metrics to export stats to Prometheus.
  • Queue:listen vs queue:work – If you need to test a job instantly, use queue:listen --once to process a single job and exit.
  • Redis CLI – Run redis-cli LLEN queues:default to see pending job count.
  • Logging – Inside a job, use Log::info('Processing user', ['id' => $this->userId]);. Logs will appear in your Laravel log file and can be aggregated with tools like Laravel Telescope.

FAQ

What is the difference between queue:work and queue:listen?

queue:work boots the framework once and re‑uses the same PHP process for each job, which is far more memory‑efficient. queue:listen boots the entire framework for every job, making it suitable only for local development.

Can I use multiple queue connections simultaneously?

Yes. You can specify the connection when dispatching: MyJob::dispatch()->onConnection('redis'). Laravel will route the job to the appropriate driver.

How does Horizon auto‑scaling work?

Horizon monitors queue length and adjusts the number of worker processes based on the scale configuration in config/horizon.php. You define thresholds like ['processes' => 5, 'memory' => 128] for each queue.

What happens if a job exceeds its timeout?

The worker will terminate the job process and mark the job as failed, moving it to the failed_jobs table if configured. You can catch this by defining a failed() method on the job class.

Is it safe to store Eloquent models directly in a job?

Storing the entire model can cause large payloads and serialization issues. Prefer passing the model's primary key and re‑fetching inside handle(). If you must pass the model, use SerializesModels trait which only stores the identifier.

How do I retry a job after a transient API error?

Throw an exception in handle(). Laravel will automatically release the job back onto the queue after the configured back‑off period. You can customize back‑off using the backoff() method.

Can I delay a job?

Yes. Use MyJob::dispatch()->delay(now()->addMinutes(10)); or set a $delay property on the job class.

Do I need to run queue:restart after code changes?

When you deploy new code, run php artisan queue:restart. This signals all workers to gracefully exit after completing their current job, allowing the next worker to boot with the updated code.

Conclusion

Laravel queues transform slow, blocking operations into reliable, asynchronous workflows that scale from a single‑server hobby app to a multi‑region enterprise system. By mastering jobs, workers, Redis, and Horizon, you gain fine‑grained control over throughput, latency, and fault tolerance. Implement the steps outlined above, follow the best practices, and continuously monitor your queues. Your users will notice faster response times, and your codebase will become more resilient.Ready to put queues into production? Start by installing Redis on your development machine, create a simple SendWelcomeEmail job, and watch Horizon's dashboard light up. If you need help migrating an existing Laravel app to use queues, reach out via the contact form on rakibahsan.xyz.