Back to Posts

Laravel Queues: Processing Heavy Tasks in the Background

Muhammad Fauzun Naja

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.

LaravelPHPQueuesRedisBackend

Muhammad Fauzun Naja

Muhammad Fauzun Naja

Backend Developer sharing insights about Laravel, PHP, and DevOps.