Laravel Queues: Processing Heavy Tasks in the Background
Modern web applications often need to perform tasks that take time — sending emails, generating PDFs, processing images, or calling external APIs. If these tasks run synchronously, users wait. Laravel Queues solve this by deferring work to background processes.
The Problem
Consider a user registration flow:
public function register(Request $request)
{
$user = User::create($request->validated());
// These add 3-5 seconds to the response
Mail::send(new WelcomeMail($user));
$this->syncToMailchimp($user);
$this->sendSlackNotification($user);
return response()->json(['message' => 'Registered!'], 201);
}
The user clicks "Submit" and stares at a loading spinner while these tasks complete. With queues, we return a response immediately and process these tasks in the background.
Queue Drivers
Laravel supports several queue backends. For most projects:
| Driver | Pros | Cons | Use Case |
|---|---|---|---|
| Database | Zero setup, no extra services | Slower, no persistence guarantees | Development, small apps |
| Redis | Fast, persistent, battle-tested | Requires Redis running | Most production apps |
| Beanstalkd | Simple, fast | Less ecosystem support | Specialized use cases |
| Amazon SQS | Fully managed, autoscaling | AWS lock-in, added cost | High-scale apps |
| Horizon | Built on Redis, UI dashboard | Redis-only | Laravel-specific apps |
Setting Up Redis Queue
First, ensure Redis is configured in config/database.php:
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
Then set your queue driver in .env:
QUEUE_CONNECTION=redis
Creating a Job
php artisan make:job ProcessPodcast
A job class has a handle method where the work happens:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
public function __construct(
public Podcast $podcast
) {}
public function handle(AudioProcessor $processor): void
{
$processor->process($this->podcast);
}
}
Dispatch it:
ProcessPodcast::dispatch($podcast);
Job Chaining & Batches
Run jobs sequentially with chains:
Bus::chain([
new ProcessPodcast($podcast),
new OptimizeAudio($podcast),
new GenerateTranscript($podcast),
])->dispatch();
Run jobs in parallel with batches (requires Laravel 8+):
$batch = Bus::batch([
new ProcessPodcast($podcast1),
new ProcessPodcast($podcast2),
new ProcessPodcast($podcast3),
])->then(function (Batch $batch) {
// All jobs completed
})->catch(function (Batch $batch, Throwable $e) {
// A job failed
})->dispatch();
Handling Failures
Always define failed method:
public function failed(\Throwable $e): void
{
Log::error('Podcast processing failed', [
'podcast_id' => $this->podcast->id,
'error' => $e->getMessage(),
]);
Notification::send(Admin::first(), new JobFailedAlert($this->podcast));
}
Running the Queue Worker
# Process jobs on the default queue
php artisan queue:work
# Process specific queues with priority
php artisan queue:work --queue=high,default,low
# Run as a daemon (production)
php artisan queue:work --daemon
For production, use Supervisor to keep the worker alive:
[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=8
user=deploy
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/worker.log
Job Middleware
Prevent duplicate jobs with middleware:
<?php
namespace App\Jobs\Middleware;
class RateLimited
{
public function handle(object $job, $next): void
{
Redis::throttle('key')
->block(10)
->allow(50)
->every(60)
->then(function () use ($job, $next) {
$next($job);
}, function () use ($job) {
$job->release(10);
});
}
}
Attach to a job:
public function middleware(): array
{
return [new RateLimited];
}
Monitoring with Horizon
Horizon provides a beautiful dashboard for Redis queues:
composer require laravel/horizon
php artisan horizon:install
Run Horizon instead of queue:work:
php artisan horizon
Access /horizon in your app to see:
- Job throughput
- Failed jobs
- Queue wait times
- Worker metrics
Real World Example: Order Processing
class ProcessOrder implements ShouldQueue
{
public function __construct(public Order $order) {}
public function handle(): void
{
// Charge the customer (retry 3x on failure)
$this->order->charge();
// Send receipt email
Mail::send(new OrderReceipt($this->order));
// Update inventory
InventoryService::decrementStock($this->order->items);
// Notify warehouse
dispatch(new PrepareShipment($this->order))
->delay(now()->addMinutes(5));
}
public function failed(\Throwable $e): void
{
$this->order->markAsFailed($e->getMessage());
Notification::send(new PaymentFailedAlert($this->order));
}
public function middleware(): array
{
return [new RateLimited];
}
}
Conclusion
Queues transform your Laravel app from synchronous bottleneck to asynchronous powerhouse. Start with Redis + Supervisor for production, add Horizon for monitoring, and use job middleware for advanced control. Your users — and your response times — will thank you.
Muhammad Fauzun Naja
Backend Developer sharing insights about Laravel, PHP, and DevOps.