Web Development

Laravel Horizon gave us visibility into queues we were flying blind on

DEV

Several client projects on Laravel run background queues for email, PDF generation, and third-party API syncing, and until this year our only visibility into them was tailing log files and occasionally querying the `failed_jobs` table by hand when something seemed off. It worked, in the sense that we could usually eventually figure out what had gone wrong, but "eventually" and "usually" are not words that belong anywhere near production queue monitoring.

What flying blind on queues actually looked like

A queued job failing silently is a specific kind of frustrating problem, because nothing about a failed background job necessarily produces a visible symptom anywhere a user or even a developer would notice immediately. An email that never sends, a PDF that never generates, a third-party sync that silently stops updating — each of these can go unnoticed for days or weeks, discovered only when someone downstream asks why something they expected to happen never did. We had at least two incidents before this year where a queue had been quietly failing for over a week before anyone noticed, purely because nothing was actively watching it.

What Horizon actually gives us

Laravel Horizon, released this year, gives us a real dashboard for Redis-backed queues — throughput graphs, job runtime, and a proper interface for inspecting and retrying failed jobs instead of writing one-off SQL. Setting it up on our existing Redis-queue projects took under an hour per project, which was a pleasant surprise given how much visibility it immediately provided in return for that small setup cost.

The specific views we check most often

  • The throughput graph, which shows job volume over time per queue and makes a sudden drop to zero immediately visible, rather than something we would previously only notice once someone complained
  • The failed jobs list, with full stack traces available directly in the dashboard rather than requiring a SSH session and a grep through log files to reconstruct what actually went wrong
  • Job runtime metrics, which surfaced a genuinely surprising finding on one client's project — a PDF generation job that had been quietly getting slower over several months as the underlying data grew, well before it was slow enough to be a visible complaint from anyone

What it already caught for us

Setting it up on our existing Redis-queue projects took under an hour per project, and it has already surfaced a couple of quietly-failing jobs on one client's site that had been failing silently for weeks with nobody noticing. The specific failure was a third-party API sync job that had started failing after the third party rotated their API credentials without notifying anyone, and the job had simply been failing and retrying on a loop in the background, invisible to anyone, until Horizon's failed jobs count made it immediately obvious the first time anyone actually looked at the dashboard.

Retrying failed jobs properly

Beyond visibility, the ability to retry a failed job directly from the dashboard, after fixing whatever caused it to fail, has removed what used to be a genuinely awkward manual process involving either re-triggering the job's original conditions or writing a one-off script to requeue it by hand. Horizon's retry button, combined with the ability to bulk-retry every job that failed for the same underlying reason, turned what used to be a small engineering task into something a less technical team member can handle confidently once the root cause is understood and fixed.

Setting up alerting on top of it

Horizon's dashboard alone still requires someone to actually look at it, which is an improvement over log-tailing but not a complete solution on its own. We paired it with a simple alerting rule watching the failed jobs count per queue, sending a notification to the relevant project's Slack channel if failures cross a small threshold within a short window, so a queue quietly failing does not depend on someone remembering to check a dashboard that, left unchecked, is not meaningfully better than the log files it replaced.

Where we go from here

Every new Laravel project with a queue now gets Horizon configured as part of initial setup rather than added later once a failure makes the gap obvious, and we are evaluating whether a similar level of visibility is worth building for our Node-based queue systems on other projects, since the value of simply being able to see what is happening in a queue, rather than inferring it from downstream symptoms, has been higher than we expected going into this.

Tagging jobs for better filtering

Once we had real dashboard visibility, we started tagging queued jobs with contextual metadata — which client account triggered a job, which specific type of email it represents — since Horizon's dashboard supports filtering by tag and this turned out to matter more in practice than we expected. A support request about "did this specific customer's confirmation email actually send" used to mean grepping logs for an order ID and hoping the right log line existed; with tagged jobs, the same question is answered by filtering Horizon's dashboard directly, in seconds rather than the several minutes a log search used to take.

Balancing queue priorities

With visibility into actual throughput per queue, we noticed one client's PDF generation queue was occasionally starving a much higher-priority email queue during traffic spikes, since both were configured with the same number of workers and PDF jobs simply take longer to process each. We reconfigured worker allocation to give the email queue dedicated capacity separate from the PDF queue, a change we would have had no clear evidence to justify making before Horizon made the actual contention visible rather than something we were only guessing at based on anecdotal complaints.

Extending the same discipline to newer projects

Every new Laravel project with a queue now gets Horizon configured as part of initial setup, and we have started applying the same underlying philosophy — actual dashboards over log-tailing, tagged jobs over anonymous ones, alerting thresholds set deliberately rather than left to whoever happens to notice a problem — to other parts of our stack, including a similar investment in structured logging for our Node-based services that had previously relied on the same reactive, log-tailing approach Laravel queues used before Horizon existed.

Sharing this internally beyond the Laravel team

The visibility win here was compelling enough that engineers working primarily on our Node-based projects sat in on a walkthrough of the Horizon dashboard specifically to see what a good queue-monitoring experience looks like, even though nothing about Horizon itself applies outside the Laravel ecosystem. That walkthrough directly informed the shape of the lighter-weight monitoring dashboard we later built for our own Node queue systems, borrowing Horizon's emphasis on per-queue throughput and an easy retry action rather than starting that design conversation from scratch. We now include a short Horizon walkthrough as part of handing off any Laravel project to a client's own internal team, so their engineers inherit the same visibility we relied on to catch problems early, rather than reverting to log-tailing once the project is no longer actively supported by us directly.

A small feature request we ended up building ourselves

Horizon's dashboard did not originally include a way to see historical throughput trends beyond a short recent window, which mattered for one client wanting to correlate queue load with marketing campaign timing over several months. We built a small internal script that periodically snapshots Horizon's metrics into our own longer-term storage, giving that client's team a simple monthly chart showing queue volume trends alongside their campaign calendar, without needing to wait for or request that feature directly from the Horizon project itself. That small internal tool has since been generalized into something we now offer as a lightweight add-on for any client wanting longer-term visibility into their own queue trends, built entirely on top of data Horizon was already collecting but had simply never been asked to retain past its default short window. It is a modest tool, built from spare capacity between client projects rather than a dedicated development effort, but the handful of clients using it have found genuine, ongoing value in it, which was more than we originally expected from what started as an internal convenience script. We expect to keep extending it modestly as more clients ask for the same kind of longer-term trend visibility.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients