Modern web applications must respond to user requests in milliseconds while still performing heavy lifting such as sending emails, generating PDFs, processing images, synchronizing with third-party APIs, and analyzing data. Blocking the HTTP request cycle for these tasks creates a poor user experience and limits scalability. This is where Laravel queues become indispensable. Laravel queues provide a clean, unified API to defer time-consuming work to background processes, allowing your application to stay fast and responsive.
In this comprehensive guide we explore Laravel queues from first principles to production-grade implementations. You will learn the core concepts, understand the underlying architecture, follow a step-by-step setup, examine real-world use cases, study production-ready code, compare queue drivers, and adopt best practices that keep your system reliable under load. Whether you are building a small SaaS or a high-traffic enterprise platform, mastering Laravel queues is a critical skill for any PHP developer.
The ecosystem around Laravel queues is mature. The framework supports multiple queue backends including database, Redis, Beanstalkd, Amazon SQS, and custom connectors. Additionally, Laravel Horizon offers a beautiful dashboard and configuration system for Redis-backed queues. By the end of this article you will be able to design a queue architecture that scales horizontally, recovers from failures, and provides observability into every job that runs.
Before writing code, you must understand the vocabulary of Laravel queues. A job is a unit of work that implements the ShouldQueue interface or is dispatched via the queue system. Jobs are typically PHP classes with a handle method containing the business logic. A queue connection defines the backend used to store and retrieve jobs. The queue name is a logical channel within a connection, allowing prioritization and separation of concerns.
A worker is a long-running PHP process that listens to a queue connection and executes jobs as they arrive. Workers are started via the Artisan command queue:work or queue:listen. The producer is any part of your application that dispatches a job, such as a controller, event listener, or console command.
Laravel provides a rich feature set: delayed jobs (->delay(now()->addMinutes(10))), scheduled jobs via the task scheduler, retries with exponential backoff, timeout handling, middleware for rate limiting, batches to group jobs and track progress, and failed job storage for post-mortem analysis. The failed_jobs table (created by the queue:failed-table migration) persists exceptions so you can retry later.
Another key concept is serialization. When you dispatch a job, Laravel serializes the job object and its properties into a string (using PHP serialization or JSON for some drivers) and stores it in the backend. When a worker picks it up, it unserializes the job and calls handle. This means you should not store complex objects like Doctrine entities; instead store identifiers and re-fetch from the database inside the job.
Finally, queue middleware lets you apply cross-cutting concerns such as throttling, skipping if a condition is met, or wrapping the job in a database transaction. Combined with Laravel Horizon, you gain per-queue concurrency limits, metrics, and auto-scaling configuration.
A typical Laravel queue architecture comprises three tiers: the application tier (producers), the broker tier (queue backend), and the worker tier (consumers). The producer dispatches a job through the Laravel queue manager, which serializes the job and pushes it to the configured broker. The broker holds the job until a worker requests the next item.
In the most common production setup, the broker is Redis. Redis is an in-memory data structure store that provides blazing-fast list operations, making it ideal for queueing. Laravel uses Redis lists or streams (with Horizon) to store jobs. Workers run as separate operating-system processes, often managed by Supervisor or systemd, and connect to the same Redis instance.
When a worker pulls a job, it marks it as reserved (or removes it depending on driver) and begins execution. Upon success, the job is deleted from the queue. If it fails, Laravel attempts retries based on the $tries property or tryAgainIf logic. After exhausting retries, the job is inserted into the failed_jobs table with the exception trace.
Horizon introduces a supervisor process that manages worker pools. You define strategies such as simple, balance, or auto to allocate processes across queues based on load. Horizon also exposes a web UI showing throughput, average runtime, and failure rates. For cloud deployments, you can pair Horizon with Laravel Forge or Envoyer to orchestrate zero-downtime releases with php artisan queue:restart.
The architecture is inherently decoupled: producers do not need to know about worker implementation, and workers can be scaled independently. This enables asynchronous processing, fault isolation, and elastic scalability.
Follow these steps to implement Laravel queues in a fresh or existing Laravel project.
- Install Laravel (if not already):
composer create-project laravel/laravel queue-demo. - Configure the queue connection in
.env: setQUEUE_CONNECTION=redis. Ensure Redis is running andREDIS_HOST,REDIS_PORTare correct. - Create the failed jobs table: run
php artisan queue:failed-tablethenphp artisan migrate. - Generate a job class:
php artisan make:job ProcessPodcast. The class will be placed inapp/Jobs. - Implement the handle method: inject dependencies, write logic, and mark the class with
implements ShouldQueue(themake:jobcommand already does this if you pass--queued). - Dispatch the job from a controller or route:
ProcessPodcast::dispatch($podcast); - Start a worker:
php artisan queue:work redis --queue=default --tries=3. Keep this process running in production using Supervisor. - Monitor: install Horizon via
composer require laravel/horizon, publish assets, and runphp artisan horizonto see the dashboard.
After completing these steps, any dispatched job will be processed asynchronously. You can verify by checking logs or the Horizon dashboard. For local development you may use the sync driver which executes jobs immediately, but never use sync in production for heavy tasks.
Laravel queues shine in numerous scenarios. Below are common patterns adopted by engineering teams.
- Transactional email sending: After a user registers, dispatch a
SendWelcomeEmailjob instead of blocking the response. If SMTP fails, the job retries without affecting the user. - Image and video processing: When a user uploads a profile picture, a job resizes it to multiple dimensions and stores them on S3. This prevents timeouts on the upload endpoint.
- Report generation: A finance dashboard triggers a nightly
GenerateMonthlyInvoiceBatchjob that aggregates data and emails PDFs. Using batches, the system shows progress bars. - Third-party sync: CRM integration jobs pull contacts from an external API with rate limiting middleware to avoid 429 errors.
- Webhook delivery: Outbound webhooks are queued with exponential backoff so transient network errors do not lose events.
- Machine learning inference: A job sends text to an OpenAI-compatible endpoint and stores the embedding in PostgreSQL for later retrieval.
In each case, the user-facing request completes in milliseconds while the background job ensures eventual consistency. This improves perceived performance and system resilience.
Below are realistic, production-grade code snippets demonstrating Laravel queues.
// app/Jobs/ProcessPodcast.phpnamespace App\Jobs;use App\Models\Podcast;use App\Services\AudioTranscoder;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Illuminate\Queue\Middleware\WithoutOverlapping;class ProcessPodcast implements ShouldQueue{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; public int $timeout = 600; public string $queue = 'media'; public function __construct(protected int $podcastId) { } public function handle(AudioTranscoder $transcoder): void { $podcast = Podcast::findOrFail($this->podcastId); $transcoder->transcode($podcast->path); $podcast->update(['processed' => true]); } public function middleware(): array { return [new WithoutOverlapping($this->podcastId)]; }}The above job uses the WithoutOverlapping middleware to prevent concurrent processing of the same podcast. It sets a dedicated queue named media so you can allocate more workers to it.
// app/Http/Controllers/PodcastController.phpnamespace App\Http\Controllers;use App\Jobs\ProcessPodcast;use App\Models\Podcast;use Illuminate\Support\Facades\Redirect;class PodcastController extends Controller{ public function store(Request $request) { $podcast = Podcast::create($request->validated()); ProcessPodcast::dispatch($podcast->id)->delay(now()->addSeconds(5)); return Redirect::route('podcasts.index')->with('status', 'Processing started.'); }}Dispatching with a small delay can help smooth traffic spikes. For batch processing:
use Illuminate\Bus\Batch;use Illuminate\Support\Facades\Bus;use App\Jobs\ExportRow;$batch = Bus::batch([])->then(function (Batch $batch) { // batch complete})->catch(function (Batch $batch, \Throwable $e) { // first failure})->name('Export CSV')->dispatch();foreach ($rows as $row) { $batch->add(new ExportRow($row));}Finally, a Supervisor configuration to keep workers alive:
[program:laravel-worker]process_name=%(program_name)s_%(process_num)02dcommand=php /var/www/html/artisan queue:work redis --queue=default,media --sleep=3 --tries=3 --max-time=3600autostart=trueautorestart=trueuser=www-datanumprocs=8redirect_stderr=truestdout_logfile=/var/log/laravel-worker.logChoosing the right queue driver depends on throughput, durability, and operational complexity. The table below compares the most common Laravel queue backends.
| Driver | Persistence | Throughput | Setup Complexity | Best For |
|---|---|---|---|---|
| Sync | In-memory (none) | N/A (blocking) | Trivial | Local testing, simple scripts |
| Database | Durable (MySQL/PostgreSQL) | Moderate | Low | Small apps without Redis |
| Redis | In-memory + optional AOF | Very High | Medium | Production, high-throughput systems |
| Beanstalkd | Memory with binlog | High | Medium | Legacy queues, simple ops |
| Amazon SQS | Durable managed service | High (network bound) | High (AWS config) | Serverless, cloud-native scaling |
| RabbitMQ (package) | Durable, AMQP features | High | High | Complex routing, enterprise ESB |
For most modern Laravel applications, Redis with Horizon provides the best balance of speed, observability, and operational simplicity.
- Keep jobs small and single-purpose. A job should do one thing; if you need multiple steps, use a batch or chain.
- Make jobs idempotent. If a job runs twice due to at-least-once delivery, it should not corrupt data. Use unique constraints or
WithoutOverlapping. - Store only IDs in job payloads. Re-query the database inside
handle to avoid stale model state and large serialization. - Set explicit timeouts and tries. Prevent runaway processes and infinite loops.
- Use dedicated queues for priority. Route critical jobs (password resets) to a high-priority queue with more workers.
- Monitor with Horizon or external APM. Track failed jobs, runtime percentile, and queue depth.
- Handle failures gracefully. Implement
failed method in jobs to alert or compensate. - Version your jobs. When changing job structure, maintain backward compatibility for unprocessed jobs.
WithoutOverlapping.handle to avoid stale model state and large serialization.failed method in jobs to alert or compensate.- Serializing Eloquent models directly. This causes huge payloads and stale data. Pass the primary key instead.
- Not running migrations for failed_jobs. Then you lose visibility into errors.
- Using sync driver in production. Defeats the purpose and kills response time.
- Forgetting to set retry_after. Redis may release a job back to queue while still processing, causing duplicates.
- Blocking workers with sleep loops. Use
queue:work --sleep=3 instead of manual sleeps inside jobs. - Ignoring memory leaks. Long-running workers accumulate memory; use
--max-jobs or --max-time to restart periodically. - Over-broad catch blocks. Swallowing exceptions hides failures from the retry system.
queue:work --sleep=3 instead of manual sleeps inside jobs.--max-jobs or --max-time to restart periodically.- Prefer Redis over database. List operations are O(1) and avoid SQL contention.
- Tune worker count. Match number of workers to CPU cores and job I/O wait. Benchmark with load tests.
- Use queue:work not queue:listen in production.
work boots the framework once; listen reboots per job (slower). - Enable Horizon balance mode. It auto-distributes processes to busy queues.
- Batch database writes. Inside jobs, use bulk inserts/updates to reduce round trips.
- Offload heavy serialization. Use JSON columns or external stores for large objects.
- Set appropriate --timeout. Should be lower than
retry_after to avoid duplicate execution.
work boots the framework once; listen reboots per job (slower).retry_after to avoid duplicate execution.- Do not embed secrets in job payloads. Store credentials in environment or secret managers; fetch at runtime.
- Encrypt Redis connections. Use TLS for Redis in production to prevent job data interception.
- Restrict failed_jobs table access. It may contain sensitive context from exceptions.
- Validate all input inside jobs. Just because a job was dispatched internally does not guarantee safe data.
- Beware of unserialize vulnerabilities. Keep Laravel updated; the framework uses secure serialization but custom PHP objects can be risky.
- Apply rate limiting middleware. Prevent abuse if jobs trigger external paid APIs.
- Use least-privilege database users for workers. Separate worker DB credentials from web user if possible.
Deploying Laravel queues requires process supervision. On Ubuntu servers, install Supervisor and add a program configuration similar to the example in the code section. Set numprocs according to your CPU. For Docker, create a service using the same image and override the command to php artisan queue:work. In Kubernetes, use a Deployment with replicas for workers separate from the web Deployment.
During zero-downtime deployments, call php artisan queue:restart after releasing new code. Workers will finish current jobs and exit; Supervisor restarts them with the new code. Horizon users should run php artisan horizon:terminate instead. Always run php artisan config:cache and route:cache to reduce boot time.
Scale horizontally by adding more worker nodes behind the same Redis. Use cloud managed Redis (Elasticache, Upstash) with persistence enabled. Monitor queue latency with Redis LLEN or Horizon metrics; set alerts if depth exceeds threshold.
- Inspect failed jobs:
php artisan queue:failed shows ID, connection, queue, and exception. - Retry specific job:
php artisan queue:retry 5 or queue:retry all. - Log inside handle: Use
info() or a dedicated channel to trace execution. - Local debugging: Set
QUEUE_CONNECTION=sync to run jobs inline with Xdebug. - Horizon UI: Check the recent jobs tab for stack traces and slow jobs.
- Check Redis directly:
redis-cli LLEN queues:default to see backlog. - Enable job timing: Use
Horizon::routeMailNotificationsTo() or custom listeners to record metrics.
php artisan queue:failed shows ID, connection, queue, and exception.php artisan queue:retry 5 or queue:retry all.info() or a dedicated channel to trace execution.QUEUE_CONNECTION=sync to run jobs inline with Xdebug.redis-cli LLEN queues:default to see backlog.Horizon::routeMailNotificationsTo() or custom listeners to record metrics.What exactly is a Laravel queue?
A Laravel queue is a system that lets you defer the execution of time-consuming tasks to background processes. It provides a unified API to push jobs to a broker and run them asynchronously via worker processes.
Which queue driver should I choose?
For production, Redis is recommended due to its speed and first-class Horizon support. Database driver is fine for low-throughput apps. SQS is excellent if you already use AWS.
What is the difference between queue:work and queue:listen?
queue:work boots the Laravel framework once and persists, pulling jobs in a loop. queue:listen restarts the framework for every job, which is simpler for code changes but slower. Use work in production.
How do I retry failed jobs?
Use php artisan queue:failed to list failures, then php artisan queue:retry {id} to push them back. You can also define $tries on the job for automatic retries.
How can I schedule jobs at specific times?
Use the Laravel Task Scheduler in routes/console.php with ->daily() or ->cron() and call dispatch inside the closure, or use ->delay() when dispatching.
Can I prioritize certain jobs?
Yes. Assign jobs to named queues (e.g., high, low) and start workers with --queue=high,low. Workers process high first. Horizon allows per-queue process allocation.
How do I test queued jobs?
Use Queue::fake() in Pest/PHPUnit to assert a job was dispatched. For integration tests, set QUEUE_CONNECTION=sync so jobs run immediately, then assert side effects.
What are job batches?
Batches let you dispatch a group of jobs and track their collective progress, with then, catch, and finally callbacks. Ideal for bulk imports or report generation with progress bars.
How do I prevent duplicate job execution?
Use the WithoutOverlapping middleware with a unique key, or implement idempotency in the handle method using database locks or unique constraints.
Is it safe to pass Eloquent models to jobs?
Technically yes, but not recommended. The model is serialized at dispatch time and may become stale. Pass the primary key and re-query inside handle for fresh data and smaller payloads.
Laravel queues transform slow, fragile request cycles into fast, resilient systems. By understanding the core concepts, selecting the right driver, following best practices, and monitoring with Horizon, you can process millions of background jobs with confidence. Start by refactoring one blocking task in your current project into a queued job, then expand to batches and dedicated worker pools.
To go deeper, review our internal guides on Laravel Performance Optimization and Redis Caching Strategies for Laravel. Implement queues today and build applications that scale effortlessly under real-world load.