If you're running a Laravel app that gets a lot of traffic – think e-commerce stores during sales, social platforms, or SaaS tools – you know how quickly things can slow down when background tasks pile up. Sending emails, processing uploads, generating reports… all these can make your app feel sluggish if not handled properly.
That's where Laravel's queue system shines. It lets you move heavy work to the background so your users get fast responses. But in high-traffic apps, a basic queue setup just isn't enough. You need optimization to avoid backlogs, crashes, and angry users.
In this guide, we'll walk through practical, real-world techniques to make your Laravel queues perform like a champ. I'll keep things simple, use everyday language, and include code examples you can copy-paste. Whether you're new to queues or already using them, you'll find something useful here.
Why Queues Matter in High-Traffic Laravel Apps
Imagine a user places an order on your site. You need to:
- Save the order
- Send a confirmation email
- Update inventory
- Notify the warehouse
- Generate a PDF invoice
If you do all of this inside the HTTP request, the user might wait 5–10 seconds for the page to load. That's terrible for user experience and SEO.
Queues fix this by letting the request finish instantly while a background worker handles the slow stuff. Laravel makes this easy with a clean API that works with multiple backends.
But when traffic spikes to thousands of requests per minute, unoptimized queues can become the bottleneck. Workers crash, jobs pile up, memory balloons, and your app grinds to a halt. Optimization prevents that.
1. Choose the Right Queue Driver
The first big decision is your queue driver. Laravel supports several out of the box:
- Database – easy to set up, but slow under heavy load
- Redis – blazing fast, feature-rich, perfect for most high-traffic apps
- Amazon SQS – great for serverless or massive scale
- Beanstalkd – lightweight and fast
For high-traffic apps, Redis is usually the winner. It's in-memory, supports pub/sub, rate limiting, and works beautifully with Laravel Horizon (more on that later).
To switch to Redis:
// .env
QUEUE_CONNECTION=redis
Install Redis on your server and the PHP extension (phpredis is faster than predis). Then run:
php artisan queue:table
php artisan migrate
If you're on Laravel 11+, the failed_jobs table is created automatically when needed.
2. Use Multiple Queues with Priorities
Not all jobs are equal. Sending a password reset email can wait, but processing a payment webhook should happen instantly.
Create multiple queues and assign priorities:
// config/queue.php
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 5,
'after_commit' => true,
],
],
Dispatch jobs to specific queues:
SendInvoiceJob::dispatch($order)->onQueue('high');
ProcessImageJob::dispatch($upload)->onQueue('low');
Start workers with priority order:
php artisan queue:work redis --queue=high,default,low
The worker will always process high queue jobs first. This keeps critical tasks moving even during traffic spikes.
3. Run Workers Reliably with Supervisor
Never run queue:work manually on a production server. If the process crashes (and it will), jobs stop processing.
Use Supervisor (on Linux) to manage workers. It restarts them automatically and can run multiple processes.
Example Supervisor config (/etc/supervisor/conf.d/laravel-queue.conf):
[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
stopwaitsecs=3600
Here we're running 8 workers. Adjust based on your server's CPU/memory. More workers = faster processing, but watch memory usage.
After saving, run:
supervisorctl reread
supervisorctl update
supervisorctl start laravel-queue:*
4. Monitor and Manage with Laravel Horizon
If you're using Redis, Laravel Horizon is a game-changer. It gives you a beautiful dashboard to see queue throughput, job runtimes, failed jobs, and more.
Install it:
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Configure balances in config/horizon.php:
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'processes' => 10,
'tries' => 3,
],
],
],
Horizon auto-balances load across workers and can pause queues during deployments. Access the dashboard at /horizon (protect it with auth).
Seeing real-time metrics helps you spot slow jobs before they become problems.
5. Optimize Your Job Code
Even with perfect setup, slow jobs will hurt performance.
Best practices:
- Keep payloads small – Pass IDs, not entire models
- Make jobs idempotent – Safe to run multiple times
- Avoid large loops inside jobs – Break into smaller jobs
- Use shouldBeEncrypted() for sensitive data
Bad example:
class ProcessOrderJob implements ShouldQueue
{
public $order; // Large Eloquent model
public function __construct(Order $order)
{
$this->order = $order;
}
}
Good example:
class ProcessOrderJob implements ShouldQueue
{
public $orderId;
public function __construct($orderId)
{
$this->orderId = $orderId;
}
public function handle()
{
$order = Order::findOrFail($this->orderId);
// process...
}
}
Smaller payload = less memory and faster serialization.
6. Use Job Batching for Bulk Operations
When you have thousands of similar tasks (e.g., importing users from CSV), use job batching.
$batch = Bus::batch([
new ImportUserJob($row1),
new ImportUserJob($row2),
// ...
])->then(function () {
// All jobs completed successfully
})->catch(function ($batch, $e) {
// First failed job
})->finally(function () {
// Batch finished
})->dispatch();
return $batch->id;
You can track progress in Horizon or via callbacks. Batching also allows canceling or retrying entire groups.
7. Handle Failed Jobs Gracefully
Jobs will fail. Network issues, API downtime, bugs – it's normal.
Set sensible retries:
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 5;
public $backoff = [10, 30, 60, 300]; // seconds
}
Or globally in the job class or worker command.
Use the failed_jobs table and Horizon to review and retry failures. You can even add custom failed job providers.
8. Add Rate Limiting and Throttling
Don't hammer third-party APIs. Use Laravel's rate limiting on jobs:
RateLimiter::for('sendgrid', function ($job) {
return Limit::perMinute(50);
});
Dispatchable job:
public function middleware()
{
return [(new ThrottleRequests(50, 1))];
}
Or use Redis-based limiting for more control.
9. Scale Horizontally
When one server isn't enough, add more queue workers behind a load balancer. All workers connect to the same Redis instance (or cluster).
For huge scale, consider Amazon SQS or separate Redis clusters for different environments.
Final Thoughts
Optimizing Laravel queues isn't a one-time task. Monitor, measure, and adjust as your traffic grows. Start with Redis + Horizon + Supervisor, prioritize critical jobs, and keep job code lean.
These techniques have helped countless apps handle massive traffic without breaking. Implement a few today and you'll notice faster response times, happier users, and fewer late-night alerts.
Happy coding!