Getting Queues Working Is Easy. Keeping Them Healthy Isn’t.
Most Laravel queue tutorials stop once a job dispatches successfully. In production, the real work is visibility: knowing when jobs are backing up, why they’re failing, and catching silent failures before they pile up unnoticed.
Installing Horizon for Redis Queues
composer require laravel/horizon
php artisan horizon:install
php artisan horizon
Horizon gives you a real-time dashboard of throughput, wait times, and failed jobs per queue — far more visibility than queue:work logs alone.
Configuring Queue-Specific Supervisors
// config/horizon.php
'environments' => [
'production' => [
'supervisor-emails' => [
'connection' => 'redis',
'queue' => ['emails'],
'balance' => 'auto',
'maxProcesses' => 5,
],
'supervisor-critical' => [
'connection' => 'redis',
'queue' => ['payments'],
'balance' => 'simple',
'maxProcesses' => 10,
],
],
],
Isolating critical queues (like payments) onto dedicated supervisors prevents a backlog in a low-priority queue from starving time-sensitive jobs.
Alerting on Queue Backlog
use Laravel\Horizon\Horizon;
Horizon::routeSlackNotificationsTo(config('services.slack.webhook'));
Horizon::routeMailNotificationsTo('oncall@example.com');
// A scheduled check for queue depth beyond a healthy threshold
$size = Queue::size('payments');
if ($size > 500) {
Notification::route('slack', config('services.slack.webhook'))
->notify(new QueueBacklogAlert('payments', $size));
}
Diagnosing Common Failure Patterns
- Jobs timing out — check
$timeouton the job class against actual execution time; a job silently killed mid-external-API-call is a common source of partial writes. - Memory leaks in long-running workers — use
--max-jobsand--max-timeflags to recycle worker processes periodically. - Duplicate job execution — usually caused by a job timing out and being retried while the original is still finishing; implement idempotency keys for anything non-idempotent, like payment charges.
php artisan queue:work redis --max-jobs=1000 --max-time=3600 --timeout=90
Idempotency for Safe Retries
public function handle(): void
{
$alreadyProcessed = Cache::has("job-processed:{$this->idempotencyKey}");
if ($alreadyProcessed) return;
PaymentGateway::charge($this->amount, $this->idempotencyKey);
Cache::put("job-processed:{$this->idempotencyKey}", true, now()->addDay());
}
Tracking Failure Trends Over Time
Failed job counts alone don’t tell you if things are getting worse. Export Horizon metrics to your existing observability stack (Prometheus, Datadog) so failure rates show up alongside your other application metrics rather than in an isolated dashboard.
Conclusion
Production queue reliability comes from visibility and isolation — dedicated supervisors for critical queues, alerting on backlog before it becomes a customer-facing problem, and idempotent job design so retries don’t cause duplicate side effects.