Queue monitoring shouldn't require SSH and log files. Laravel Horizon provides a beautiful dashboard for Redis queues with real-time metrics, failed job management, and automatic worker balancing. At ZIRA Software, Horizon has transformed queue management from frustrating detective work to elegant monitoring.
Why Laravel Horizon?
Problems without Horizon:
- No visibility into queue status
- Manual worker management
- Difficult to debug failed jobs
- No performance metrics
- Command-line only monitoring
- Worker process management via Supervisor
Horizon provides:
- Beautiful web dashboard
- Real-time metrics
- Job throughput graphs
- Failed job management with retry
- Automatic worker balancing
- Job tagging and monitoring
- Wait time tracking
- Worker configuration per environment
Installation
composer require laravel/horizon
Publish assets:
php artisan horizon:install
php artisan migrate
Service provider auto-registered in Laravel 5.5+.
Configuration
config/horizon.php:
<?php
return [
'use' => 'default',
'prefix' => env('HORIZON_PREFIX', 'horizon:'),
'middleware' => ['web'],
'waits' => [
'redis:default' => 60,
],
'trim' => [
'recent' => 60,
'failed' => 10080,
'monitored' => 10080,
],
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'simple',
'processes' => 10,
'tries' => 3,
],
],
'local' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'simple',
'processes' => 3,
'tries' => 3,
],
],
],
];
Running Horizon
Start Horizon:
php artisan horizon
As daemon:
php artisan horizon &
Monitor output:
php artisan horizon:status
Terminate:
php artisan horizon:terminate
Supervisor Configuration
Production deployment needs Supervisor:
/etc/supervisor/conf.d/horizon.conf:
[program:horizon]
process_name=%(program_name)s
command=php /var/www/app/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/horizon.log
stopwaitsecs=3600
Reload Supervisor:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start horizon
Dashboard
Access dashboard:
http://yourapp.com/horizon
Shows:
- Jobs per minute (real-time graph)
- Recent jobs
- Failed jobs
- Job throughput
- Wait times
- Worker processes
- Memory usage
Authentication
Protect dashboard in production:
app/Providers/HorizonServiceProvider.php:
<?php namespace App\Providers;
use Laravel\Horizon\Horizon;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class HorizonServiceProvider extends ServiceProvider
{
public function boot()
{
Horizon::auth(function ($request) {
return Gate::check('viewHorizon', [$request->user()]);
});
}
}
Gate definition:
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
'admin@example.com',
]);
});
Worker Configuration
Multiple supervisors:
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'processes' => 10,
'tries' => 3,
'timeout' => 300,
],
'supervisor-2' => [
'connection' => 'redis',
'queue' => ['emails', 'notifications'],
'balance' => 'simple',
'processes' => 5,
'tries' => 3,
'timeout' => 60,
],
],
],
Balancing strategies:
- simple: Evenly distribute
- auto: Automatically balance based on workload
- false: No balancing
Job Tagging
Tag jobs for monitoring:
<?php namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Laravel\Horizon\Contracts\Taggable;
class ProcessPodcast implements ShouldQueue, Taggable
{
use Queueable;
protected $podcast;
public function tags()
{
return ['podcast:' . $this->podcast->id, 'user:' . $this->podcast->user_id];
}
public function handle()
{
// Process podcast
}
}
Search by tag in dashboard:
- Filter by
podcast:123 - Filter by
user:456
Failed Jobs
View in dashboard:
- See stack trace
- View job payload
- Retry individual jobs
- Retry all failed jobs
Retry from command line:
# Retry specific job
php artisan horizon:forget JOB_ID
# Retry all failed
php artisan queue:retry all
Auto-retry logic:
public function retryUntil()
{
return now()->addMinutes(10);
}
public $tries = 5;
public $timeout = 120;
Metrics
Dashboard shows:
- Jobs Per Minute: Real-time throughput
- Recent Jobs: Last 1000 processed
- Failed Jobs: All failures with retry
- Wait Times: Average time in queue
- Processed Jobs: Total count
- Workers: Active processes
Monitor wait times:
'waits' => [
'redis:default' => 60, // Alert if wait > 60 seconds
'redis:emails' => 30,
],
Notifications
Get notified of long waits:
use Laravel\Horizon\Contracts\JobProcessed;
use Laravel\Horizon\Contracts\SupervisorLooped;
class HorizonServiceProvider extends ServiceProvider
{
public function boot()
{
Horizon::night();
Horizon::routeSlackNotificationsTo(
'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
);
Horizon::routeSmsNotificationsTo('15556667777');
}
}
Tags vs Queues
Use queues for:
- Different priorities (high, default, low)
- Different worker configurations
- Resource isolation
Use tags for:
- Filtering/searching
- Monitoring specific job types
- User-based filtering
- Debugging
Pruning
Automatically prune old data:
'trim' => [
'recent' => 60, // Keep recent jobs for 1 hour
'failed' => 10080, // Keep failed jobs for 1 week
'monitored' => 10080, // Keep monitored jobs for 1 week
],
Manual pruning:
php artisan horizon:purge
Deployment
Zero-downtime deployment:
#!/bin/bash
# Pull latest code
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Terminate Horizon gracefully
php artisan horizon:terminate
# Wait for jobs to finish (up to 60 seconds)
sleep 60
# Supervisor will restart Horizon automatically
In deploy script:
# After deployment
php artisan queue:restart # Restart all workers
php artisan horizon:terminate # Graceful shutdown
Testing
Fake queues:
use Illuminate\Support\Facades\Queue;
public function test_podcast_is_processed()
{
Queue::fake();
// Dispatch job
ProcessPodcast::dispatch($podcast);
// Assert job was dispatched
Queue::assertPushed(ProcessPodcast::class, function ($job) use ($podcast) {
return $job->podcast->id === $podcast->id;
});
}
Monitoring Best Practices
- Alert on wait times - Set reasonable thresholds
- Monitor failed jobs - Review daily
- Track throughput - Identify bottlenecks
- Use tags - For important job types
- Set timeouts - Prevent hanging jobs
- Configure retries - Intelligent retry logic
- Prune regularly - Keep dashboard fast
Common Issues
Horizon not processing:
# Check if running
php artisan horizon:status
# View logs
tail -f storage/logs/horizon.log
# Restart
php artisan horizon:terminate
php artisan horizon
Jobs stuck in queue:
- Check worker configuration
- Verify queue names match
- Check for exceptions in jobs
- Increase processes if needed
High memory usage:
- Reduce processes
- Add memory limits
- Implement chunking
- Monitor with tools
Production Tips
Use separate Redis database:
'redis' => [
'client' => 'predis',
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'database' => 0,
],
'horizon' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'database' => 1, // Separate database
],
],
Monitor with New Relic/DataDog:
public function handle()
{
\NewRelic::addCustomTracer('ProcessPodcast');
// Job logic
}
Conclusion
Laravel Horizon transforms queue management from blind command-line work to beautiful, informative monitoring. Real-time metrics, failed job retry, and automatic balancing make queue operations delightful. At ZIRA Software, Horizon has eliminated queue-related debugging sessions and provided visibility that drives optimization.
Install Horizon, configure workers appropriately, and enjoy effortless queue monitoring.
Need robust queue processing for your application? Contact ZIRA Software to discuss queue architecture, worker optimization, and reliable background job processing.