Background jobs are invisible by design. They run outside the request cycle, handle the heavy work — sending emails, processing payments, syncing data — so your users don't have to wait. That's the upside.

The downside: when they fail, nobody knows. Your app looks fine on the surface. The email never sent. The invoice never generated. The sync never ran. Laravel Horizon fixes the visibility problem and gives you control over the whole thing.

What Horizon Actually Is

Horizon is a dashboard and supervisor for Laravel queues. It monitors your jobs in real-time, tracks throughput, surfaces failures, and lets you retry failed jobs from a UI without touching the server. It runs on top of Redis-backed queues.

If you're running any background processing in production without Horizon, you're flying blind.

Getting Set Up

composer require laravel/horizon
php artisan horizon:install
php artisan migrate

That publishes the config and assets. Then you need Horizon running as a persistent process — typically via Supervisor on your server. For local development, php artisan horizon in a terminal tab works fine.

The dashboard lives at /horizon by default. Lock it down in app/Providers/HorizonServiceProvider.php:

protected function gate(): void
{
    Gate::define('viewHorizon', function ($user) {
        return in_array($user->email, [
            '[email protected]',
        ]);
    });
}

Only the emails you list can access it. Simple, effective.

Configuring Workers and Queues

Horizon's config (config/horizon.php) is where you define how many workers to run and which queues get priority:

'environments' => [
    'production' => [
        'supervisor-1' => [
            'maxProcesses' => 10,
            'queue'        => ['critical', 'default', 'low'],
            'balance'      => 'auto',
        ],
    ],
],

The balance setting is one of Horizon's best features. Set it to auto and Horizon dynamically scales workers between queues based on load. If your critical queue backs up, it pulls resources from low. You don't have to tune this manually.

What Happens When a Job Fails

By default, Laravel retries a failed job a few times before marking it as failed. Horizon surfaces those failures with full stack traces, job payloads, and timestamps. You can see exactly what went wrong and retry with one click.

Even better: configure alerts so you know immediately. Add this to your AppServiceProvider:

Horizon::night(function () {
    // Runs when Horizon goes into "night mode" (idle)
});

Or use the simpler approach — set up a notification when the queue failure rate spikes. The Horizon docs cover this well.

The key mindset shift is moving from "hope it worked" to "I'll know within minutes if it didn't."

What You Should Be Running Through Queues

If you're not using queues much yet, here's what belongs there: email sending, PDF generation, report building, third-party API calls (webhooks, payment processing follow-ups), image resizing, CSV imports, anything that takes more than a second.

Moving these out of the request cycle makes your app feel fast. Horizon makes sure they actually complete.

The 3am Scenario

Your scheduled job runs at 2:47am to generate monthly invoices. A third-party API returns a 500 error. The job fails. Without Horizon, you find out Monday when a client emails asking where their invoice is.

With Horizon, the job fails, gets logged, and when you open your laptop in the morning the failed jobs tab has it waiting with the full error. You retry, it runs, the invoice generates. Client never knows. That's the difference.

Background jobs are only reliable if you can see them. If you need help setting up proper queue monitoring, that's a fast engagement with real operational impact.

Horizon and queue monitoring are part of what Pixelworx handles under ongoing maintenance and support — the infrastructure work that keeps a production application healthy after launch.