Queues and Jobs in Laravel for Asynchronous Processing
Learn how to handle heavy processing with queues and jobs in Laravel. We cover drivers like Redis and database, job middleware, batching, chaining, failure handling, and monitoring with Horizon.
Written by Francisco Zapata
In modern web applications, many tasks should not run during the lifecycle of an HTTP request: sending emails, processing images, generating PDF reports, or syncing data with external services. Laravel's queue system lets you defer these operations to background processes, dramatically improving your application's response times.
Configuring Queue Drivers
Laravel supports multiple queue backends, each with its own strengths. Configuration lives in config/queue.php.
Database Driver
The simplest driver to get started with, as it requires no additional external services.
php artisan make:queue-table
php artisan migrate
Set the driver in your .env:
QUEUE_CONNECTION=database
Redis Driver
Redis is the recommended choice for production due to its speed and efficiency. It requires either the phpredis extension or the predis package.
composer require predis/predis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
SQS Driver (Amazon)
For AWS-hosted applications, SQS provides a fully managed and highly scalable queue service.
composer require aws/aws-sdk-php
QUEUE_CONNECTION=sqs
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
SQS_QUEUE=https://sqs.us-east-1.amazonaws.com/your-account/your-queue
AWS_DEFAULT_REGION=us-east-1
Creating Jobs
Jobs are classes that encapsulate the logic of a deferred task. Each job implements the ShouldQueue interface and uses the Queueable trait.
php artisan make:job ProcessPodcast
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $timeout = 300;
public int $maxExceptions = 2;
public function __construct(
public Podcast $podcast
) {}
public function handle(AudioProcessor $processor): void
{
Log::info("Processing podcast: {$this->podcast->title}");
$processedPath = $processor->optimize(
$this->podcast->audio_path
);
$this->podcast->update([
'processed_path' => $processedPath,
'status' => 'processed',
'processed_at' => now(),
]);
Log::info("Podcast processed successfully: {$this->podcast->id}");
}
public function failed(\Throwable $exception): void
{
Log::error("Failed to process podcast {$this->podcast->id}: {$exception->getMessage()}");
$this->podcast->update(['status' => 'failed']);
}
}
Dispatching Jobs
Laravel provides multiple ways to dispatch jobs to the queue depending on your context and requirements.
use App\Jobs\ProcessPodcast;
// Basic dispatch
ProcessPodcast::dispatch($podcast);
// Conditional dispatch
ProcessPodcast::dispatchIf($podcast->needsProcessing(), $podcast);
ProcessPodcast::dispatchUnless($podcast->isProcessed(), $podcast);
// Delayed dispatch
ProcessPodcast::dispatch($podcast)
->delay(now()->addMinutes(10));
// Dispatch to a specific queue
ProcessPodcast::dispatch($podcast)
->onQueue('audio-processing');
// Dispatch to a specific connection
ProcessPodcast::dispatch($podcast)
->onConnection('redis')
->onQueue('high');
// Dispatch after the HTTP response
ProcessPodcast::dispatchAfterResponse($podcast);
// Synchronous dispatch (no queue)
ProcessPodcast::dispatchSync($podcast);
Dispatching Within Transactions
When working with database transactions, it is crucial to ensure jobs are only dispatched when the transaction commits successfully.
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($podcast) {
$podcast->update(['status' => 'queued']);
ProcessPodcast::dispatch($podcast)->afterCommit();
});
Job Middleware
Job middleware lets you wrap job execution with reusable logic such as rate limiting, overlap prevention, or exception handling.
Rate Limiting
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
public function __construct(
public string $key = 'default',
public int $allow = 10,
public int $every = 60,
) {}
public function handle(object $job, Closure $next): void
{
Redis::throttle($this->key)
->block(0)
->allow($this->allow)
->every($this->every)
->then(
fn () => $next($job),
fn () => $job->release(30)
);
}
}
Using Built-in Middleware
<?php
namespace App\Jobs;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
class SyncExternalData implements ShouldQueue
{
use Queueable;
public function middleware(): array
{
return [
new RateLimited('external-api'),
(new WithoutOverlapping($this->account->id))
->releaseAfter(60)
->expireAfter(300),
(new ThrottlesExceptions(5, 10 * 60))
->backoff(2),
];
}
public function handle(): void
{
// Sync data with external service...
}
}
Job Chaining: Sequential Execution
Job chaining lets you run a series of tasks in order, where each job only executes if the previous one succeeded.
use App\Jobs\ConvertVideo;
use App\Jobs\GenerateThumbnail;
use App\Jobs\NotifyUser;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ConvertVideo($video),
new GenerateThumbnail($video),
new NotifyUser($video->user, 'Your video is ready'),
])->onQueue('video-processing')
->catch(function (\Throwable $e) use ($video) {
$video->update(['status' => 'failed']);
Log::error("Video pipeline failed: {$e->getMessage()}");
})
->dispatch();
Job Batching: Bulk Processing
Batches let you dispatch a group of jobs and track their collective progress, which is ideal for mass imports or file processing workflows.
Setup
php artisan make:queue-batches-table
php artisan migrate
Creating Batchable Jobs
<?php
namespace App\Jobs;
use App\Models\Product;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ImportProduct implements ShouldQueue
{
use Batchable, Queueable;
public function __construct(
public array $productData
) {}
public function handle(): void
{
if ($this->batch()?->cancelled()) {
return;
}
Product::updateOrCreate(
['sku' => $this->productData['sku']],
$this->productData
);
}
}
Dispatching a Batch
use App\Jobs\ImportProduct;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
public function importProducts(Request $request)
{
$products = $this->parseCSV($request->file('csv'));
$jobs = collect($products)->map(
fn ($data) => new ImportProduct($data)
)->toArray();
$batch = Bus::batch($jobs)
->before(function (Batch $batch) {
Log::info("Batch {$batch->id} started with {$batch->totalJobs} jobs.");
})
->progress(function (Batch $batch) {
Log::info("Batch {$batch->id}: {$batch->progress()}% complete.");
})
->then(function (Batch $batch) {
Log::info("Batch {$batch->id} completed successfully.");
Notification::send($batch->options['user'], new ImportComplete());
})
->catch(function (Batch $batch, \Throwable $e) {
Log::error("Batch {$batch->id} failed: {$e->getMessage()}");
})
->finally(function (Batch $batch) {
Log::info("Batch {$batch->id} finished. Failures: {$batch->failedJobs}");
})
->name('Product Import')
->allowFailures()
->dispatch();
return response()->json([
'batch_id' => $batch->id,
'total_jobs' => $batch->totalJobs,
]);
}
Querying Batch Status
Route::get('/batch/{batchId}', function (string $batchId) {
$batch = Bus::findBatch($batchId);
return response()->json([
'id' => $batch->id,
'name' => $batch->name,
'total' => $batch->totalJobs,
'pending' => $batch->pendingJobs,
'failed' => $batch->failedJobs,
'progress' => $batch->progress(),
'finished' => $batch->finished(),
]);
});
Handling Failed Jobs
Laravel stores jobs that fail after exhausting their retries, allowing you to investigate and retry failures later.
# View failed jobs
php artisan queue:failed
# Retry a specific job
php artisan queue:retry 5
# Retry all failed jobs
php artisan queue:retry all
# Prune old failed jobs
php artisan queue:prune-failed --hours=48
Time-Based Retries
class SendNotification implements ShouldQueue
{
use Queueable;
public function retryUntil(): \DateTime
{
return now()->addHours(6);
}
public function backoff(): array
{
return [30, 60, 300]; // Seconds between retries
}
}
Unique Jobs
Prevent duplicates on the queue for jobs that should not run concurrently.
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public int $uniqueFor = 3600;
public function __construct(
public Product $product
) {}
public function uniqueId(): string
{
return $this->product->id;
}
}
Monitoring with Horizon
Laravel Horizon provides a real-time dashboard for monitoring and managing Redis-powered queues.
composer require laravel/horizon
php artisan horizon:install
php artisan horizon
Horizon lets you visualize throughput, execution times, failures, and configure supervisors for different queues. It is an indispensable tool for production applications that rely heavily on asynchronous processing.
Running the Worker
# Basic worker
php artisan queue:work
# With production options
php artisan queue:work redis --queue=high,default,low --tries=3 --timeout=90 --max-jobs=1000 --max-time=3600
Conclusion
Laravel's queues and jobs transform the user experience by moving heavy operations out of the HTTP request cycle. From file processing to notifications, the queue system offers flexibility through drivers like Redis and SQS, control through middleware and batching, and reliability through automatic retries and failure handling. Combined with Horizon for monitoring, you have everything you need to build applications that scale efficiently.
Comments (0)
Leave a comment
Be the first to comment