
Background queues are easy to forget about. Your web application is up, users can log in, pages load — and meanwhile your queue workers have been failing silently for hours. Emails aren't being sent. Reports aren't generating. Scheduled jobs are piling up in the failed jobs table.
Laravel Horizon gives you visibility into your Redis-backed queues. Combined with proper uptime monitoring, you can catch queue problems before they turn into user-facing incidents.
Horizon is a first-party Laravel package that provides a dashboard and supervisor for Redis queues. It shows you:
It's installed alongside your Laravel application and accessible at /horizon (with appropriate auth middleware in production).
Install via Composer:
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Configure queues in config/horizon.php:
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
],
Start Horizon:
php artisan horizon
In production, run Horizon as a supervised process with Supervisor or similar:
[program:horizon]
process_name=%(program_name)s
command=php /var/www/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/horizon.log
stopwaitsecs=3600
Failed jobs are the primary signal that something has gone wrong in your queue processing. Horizon's dashboard shows them, but you need a way to be alerted rather than having to check manually.
Laravel's queue system fires events when jobs fail. Hook into the Queue::failing event:
// In AppServiceProvider::boot()
use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\Events\JobFailed;
Queue::failing(function (JobFailed $event) {
// Send alert email, Slack notification, etc.
Log::error('Queue job failed', [
'job' => $event->job->resolveName(),
'exception' => $event->exception->getMessage(),
'queue' => $event->job->getQueue(),
]);
// Or notify your team:
// Notification::route('mail', '[email protected]')
// ->notify(new JobFailedNotification($event));
});
A single failed job might be a transient error. A growing failed jobs table is a symptom of a systemic problem. Track the count:
// Artisan command to check failed job counts
use Illuminate\Support\Facades\DB;
$failedCount = DB::table('failed_jobs')
->where('failed_at', '>=', now()->subHour())
->count();
if ($failedCount > 10) {
// Alert — too many failures in the last hour
}
Schedule this as a regular health check:
// In Console/Kernel.php
$schedule->command('queue:failed-check')->everyFiveMinutes();
The most important monitoring addition is a health check endpoint that confirms Horizon is running:
// In routes/web.php or routes/api.php
Route::get('/health/horizon', function () {
$status = \Laravel\Horizon\Contracts\MasterSupervisorRepository::class;
$masters = app($status)->all();
if (empty($masters)) {
return response()->json([
'status' => 'error',
'message' => 'Horizon is not running'
], 503);
}
$failedLastHour = \DB::table('failed_jobs')
->where('failed_at', '>=', now()->subHour())
->count();
return response()->json([
'status' => 'ok',
'horizon' => 'running',
'failed_last_hour' => $failedLastHour,
]);
})->middleware('auth.basic.once');
Point an uptime monitor at /health/horizon to verify Horizon is actively running, not just that your web server is up.
Laravel's task scheduler (managed via php artisan schedule:run triggered by a cron) can fail silently. The cron job stops, scheduled tasks don't run, but your web application continues working — and you might not notice for days.
Use heartbeat-style monitoring for your scheduler. Add a ping to a monitoring service at the end of critical scheduled jobs:
$schedule->command('reports:generate')
->daily()
->thenPing('https://your-heartbeat-url');
See how to monitor cron jobs for the full approach.
PHP isn't designed for long-running processes by default. Queue workers can accumulate memory over time, eventually hitting their memory limit and stopping. Horizon handles this by restarting workers when they exceed the configured memory limit:
// config/horizon.php
'memory' => 128, // MB — restart workers that exceed this
Monitor for workers that are restarting frequently — repeated restarts indicate a memory leak in a job class that needs investigation.
Queue monitoring is one layer. You also need:
If you're deploying Laravel on Railway or Render, those platforms add their own monitoring layer — but they don't monitor Horizon specifically, so the health check endpoint approach is still required.
Domain Monitor monitors your Laravel application's uptime alongside your Horizon health check. Add two monitors:
/health/horizon endpoint — catches queue processing failuresCreate a free account and set both up. You'll get immediate alerts if either the web layer or the queue layer goes down — covering the two most common failure modes for Laravel applications.
For broader Laravel monitoring guidance, see how to monitor Laravel applications and uptime monitoring best practices. If you're running queue workers across multiple frameworks — or want to see how the Horizon approach compares to BullMQ and Celery — see how to monitor queue workers on Laravel, Node.js, and Python apps.
A subdomain takeover lets an attacker claim your subdomain by exploiting dangling DNS records. Learn how it happens, real-world examples, and how DNS monitoring detects it.
Read moreMean time to detect (MTTD) measures how long it takes to discover an incident after it starts. Reducing MTTD is one of the highest-leverage improvements in reliability engineering.
Read moreBlack box monitoring tests your systems from the outside, the way users experience them — without access to internal code or infrastructure. Learn how it works and when to use it.
Read moreLooking to monitor your website and domains? Join our platform and start today.