July 2025
·2 min read
Laravel Queues & Jobs: Background Processing That Scales
Offload slow tasks (emails, PDF generation, webhooks) from your request cycle using Laravel Queues, Redis, and Horizon — with retry, failure, and monitoring strategies.
Introduction
- Synchronous request handling blocks users waiting for email sends, image processing, or third-party API calls.
- Laravel's queue system pushes work to a background worker process — the HTTP response returns immediately.
- Stack covered: Laravel 11, Redis as the queue driver, Horizon for monitoring.
Queue Drivers
| Driver | Best for |
|---|---|
sync | Local dev (no worker needed) |
database | Simple deployments (no Redis) |
redis | Production — fast, reliable |
sqs | AWS serverless deployments |
- Set
QUEUE_CONNECTION=redisin.env. - Install Predis or phpredis:
composer require predis/predis.
Creating a Job
php artisan make:job SendWelcomeEmail.
// app/Jobs/SendWelcomeEmail.php
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3; // retry up to 3 times
public int $backoff = 60; // wait 60s between retries
public function __construct(private readonly User $user) {}
public function handle(Mailer $mailer): void
{
$mailer->to($this->user)->send(new WelcomeMail($this->user));
}
public function failed(Throwable $exception): void
{
Log::error("SendWelcomeEmail failed for user {$this->user->id}", [
'exception' => $exception->getMessage(),
]);
}
}Dispatching Jobs
// Dispatch to default queue
SendWelcomeEmail::dispatch($user);
// Dispatch to a named queue with a delay
SendWelcomeEmail::dispatch($user)
->onQueue('emails')
->delay(now()->addMinutes(5));
// Dispatch synchronously (bypass queue, useful in tests)
SendWelcomeEmail::dispatchSync($user);Running Workers
# Basic worker
php artisan queue:work redis --queue=emails,default --tries=3
# Process one job and exit (good for Kubernetes Jobs / cron)
php artisan queue:work --once
# Daemon mode with graceful timeout
php artisan queue:work --timeout=90 --sleep=3Queues with Laravel Horizon
- Horizon: a dashboard + supervisor for Redis queues.
composer require laravel/horizon && php artisan horizon:install.- Configure worker pools in
config/horizon.php:
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['emails', 'default'],
'balance' => 'auto',
'processes' => 10,
'tries' => 3,
],
],
],- Start Horizon:
php artisan horizon(manage with Supervisor in production). - Dashboard at
/horizon— monitor throughput, failed jobs, wait times.
Failed Jobs
- Failed jobs are stored in the
failed_jobstable (or Redis). php artisan queue:failed— list failed jobs.php artisan queue:retry {id}— retry a specific job.php artisan queue:retry all— retry all failed jobs.- Set up
FailedJobsProviderto notify Slack/email on failure.
Job Chaining and Batches
// Chain: job B runs only after job A succeeds
Bus::chain([
new ProcessUpload($file),
new GenerateThumbnail($file),
new NotifyUser($user),
])->dispatch();
// Batch: fan-out parallel jobs with a final callback
$batch = Bus::batch([
new ProcessChunk($chunk1),
new ProcessChunk($chunk2),
])->then(fn(Batch $b) => NotifyCompletion::dispatch())
->catch(fn(Batch $b, Throwable $e) => Log::error($e))
->dispatch();Rate Limiting Jobs
- Use
RateLimiterto prevent hammering third-party APIs.
public function handle(): void
{
Redis::throttle('stripe-api')->allow(100)->every(60)->then(
fn() => $this->chargeCard(),
fn() => $this->release(30) // re-queue after 30s
);
}Dockerizing Workers
- Run a separate worker container alongside the app container.
CMD ["php", "artisan", "queue:work", "--sleep=3", "--tries=3"]in a workerDockerfile.- In
docker-compose.yml: share the same app image, override thecommand.
Testing Queued Jobs
Queue::fake();
SendWelcomeEmail::dispatch($user);
Queue::assertPushed(SendWelcomeEmail::class);
Queue::assertPushedOn('emails', SendWelcomeEmail::class);Conclusion
- Queues are the primary tool for keeping p99 API response times low.
- Redis + Horizon is the production standard for Laravel — install it before you need it.
- Monitor failed jobs actively; silent failures = frustrated users.
- Next steps: add
ShouldBeUniqueUntilProcessingto prevent duplicate jobs for idempotent operations.