Laravel Horizon Job Failures Troubleshooting Guide
Callum Davies
Jobs are silently failing or hanging in Laravel Horizon on production, causing delayed processing and missed SLAs. The root cause is often a mis‑configured Redis connection, supervisor timeout, or an unhandled exception in the job payload. This guide shows how to reproduce the failure, validate your Horizon/Redis setup, adjust supervisor settings, and set up reliable alerts.
TL;DR / Quick Summary
Verify Redis connectivity (
redis-cli ping).Run the validation script (
php scripts/horizon-check.php).Ensure
config/horizon.phphas appropriatewait,sleep, andtimeoutvalues.Update Docker supervisor config to increase
stopwaitsecs.Hook into
JobFailedevent to send Slack/Email alerts.
Reproduce a Failing Job
Example Job (app/Jobs/ProcessReport.php)
<?php
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Exception;
class ProcessReport implements ShouldQueue
{
use InteractsWithQueue, SerializesModels, Batchable;
public $tries = 3;
public $timeout = 30; // seconds
public function handle()
{
// Simulate a Redis timeout
$client = redis()->connection();
$client->set('test', str_repeat('x', 10_000_000)); // may exceed memory limit
if (random_int(0, 1)) {
throw new Exception('Random failure for debugging');
}
}
}Dispatch the job from a route or tinker:
php artisan tinker
>>> \App\Jobs\ProcessReport::dispatch();If Horizon shows the job as processing forever, the setup is broken.
Validate Horizon & Redis Settings
Validation Script (scripts/horizon-check.php)
<?php // scripts/horizon-check.php
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
use Illuminate\Support\Facades\Redis;
use Laravel\Horizon\Horizon;
function echoResult(string $label, bool $ok)
{
echo $label . ': ' . ($ok ? "✅ OK" : "❌ FAIL") . PHP_EOL;
}
// 1. Redis connection
try {
$pong = Redis::connection()->ping();
echoResult('Redis ping', $pong === 'PONG');
} catch (Throwable $e) {
echoResult('Redis ping', false);
echo $e->getMessage() . PHP_EOL;
}
// 2. Horizon config sanity
$config = config('horizon');
$required = ['wait', 'sleep', 'trim.threshold'];
foreach ($required as $key) {
echoResult("Horizon config $key", isset($config[$key]));
}
// 3. Supervisor process count
$processes = collect(Horizon::supervisorNames())
->map(fn($name) => Horizon::supervisor($name)->processes())
->sum();
echoResult('Supervisor processes > 0', $processes > 0);Run it inside the container:
docker exec -it app php scripts/horizon-check.phpAll checks must return ✅. Any ❌ indicates the exact point to fix.
Custom Supervisor Configuration for Latency Handling
Create a supervisor config file that Horizon will read (docker/worker-supervisor.conf):
[program:horizon]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan horizon
numprocs=4
autostart=true
autorestart=true
startsecs=5
stopwaitsecs=30 ; increase from default 10 to allow long jobs
stdout_logfile=/var/log/horizon_stdout.log
stderr_logfile=/var/log/horizon_stderr.log
environment=APP_ENV="production",APP_DEBUG="false"Rebuild and restart the container:
docker compose up -d --build
supervisorctl reread && supervisorctl updateThe higher stopwaitsecs prevents Horizon from killing jobs that exceed the default 10‑second grace period.
Alerting Pattern for Failed Jobs
1. Register a listener (app/Listeners/NotifyFailedJob.php)
<?php
namespace App\Listeners;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\Notification;
use App\Notifications\JobFailedNotification;
class NotifyFailedJob
{
public function handle(JobFailed $event)
{
Notification::route('slack', config('services.slack.webhook'))
->notify(new JobFailedNotification($event->connectionName, $event->job->resolveName(), $event->exception->getMessage()));
}
}2. Notification (app/Notifications/JobFailedNotification.php)
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\SlackMessage;
class JobFailedNotification extends Notification
{
private $connection;
private $job;
private $error;
public function __construct(string $connection, string $job, string $error)
{
$this->connection = $connection;
$this->job = $job;
$this->error = $error;
}
public function via($notifiable)
{
return ['slack'];
}
public function toSlack($notifiable)
{
return (new SlackMessage)
->error()
->content('Laravel Horizon job failed')
->attachment(function ($attachment) {
$attachment->title('Job Details')
->fields([
'Connection' => $this->connection,
'Job' => $this->job,
'Error' => $this->error,
]);
});
}
}3. Register the listener in EventServiceProvider
protected $listen = [
\Illuminate\Queue\Events\JobFailed::class => [
\App\Listeners\NotifyFailedJob::class,
],
];Now every failed job triggers a Slack alert, giving you immediate visibility.
Configuration Comparison Table
Setting | Default (Laravel 11) | Recommended for Production |
|---|---|---|
|
|
|
|
|
|
|
| Align with job |
|
|
|
|
|
|
Common Pitfalls & Troubleshooting
Redis connection refused – Verify Docker network alias (
redis) matchesconfig/database.phpredis.host. Usedocker exec redis redis-cli ping.Horizon shows jobs but
failed_jobstable stays empty – EnsureQUEUE_CONNECTION=redisandHORIZON_QUEUE=redisare set; also runphp artisan queue:failed-table && php artisan migrate.Supervisor kills jobs before completion – Increase
stopwaitsecsand match job$timeout.Missing
php artisan horizonprocess – Check that the supervisor config file is mounted into the container and thatsupervisorctl statuslistshorizonprocesses.Failed jobs not retried – Set
$trieson the job class and configureretry_afterinconfig/queue.php.
FAQ
Why do failed jobs not appear in Horizon's UI? Because the
failed_jobstable is missing or the queue connection is mis‑configured; run the migration and ensureQUEUE_CONNECTION=redis.How can I see the exact exception that caused a failure? Use
php artisan horizon:failedor query thefailed_jobstable; theexceptioncolumn contains the stack trace.What Redis setting most often leads to silent job stalls? An exhausted
maxmemorypolicy that evicts keys; increasemaxmemoryor switch tonoevictioninredis.conf.Do I need to restart Horizon after changing
config/horizon.php? Yes, runphp artisan horizon:terminatewhich gracefully restarts all workers with the new config.
Related Engineering Guides
Connect with KoderSolution 🚀
Master modern software engineering, scalable architectures, and advanced backend systems.
Read more architectural deep-dives, systems tutorials, and engineering guides on kodersolution.com.
Subscribe to @kodersolution for concise, animated system design breakdowns and technical shorts.
Follow our official page for daily development tips, architecture discussions, and community updates.
Recommended Resources & Courses
Callum Davies
PHP & Laravel practitioner since v5.4. Big believer in thin controllers, explicit return types, and boring modular architecture that doesn't page you on weekends.