Queue visibility before queue complexity

Distinguish waiting from execution time, record bounded failure context, and use Horizon only where its Redis queue requirement fits.
Measure the delay the user experiences
A user waiting for an export cares about time from request to usable file. Worker execution time captures only part of that interval. Separate dispatch-to-start delay, execution duration, retries, and final completion. A queue full of delayed jobs is also different from a backlog ready to run.
Record the job type and a correlation identifier at dispatch, then connect processing and completion events to that identifier. Prefer an opaque identifier over an email address or full payload. Aggregate metrics by bounded dimensions such as queue and job type; put individual identifiers in access-controlled logs where they do not explode metric cardinality.
Before adding workers, ask which component consumes the time. Rising wait with stable execution suggests capacity or scheduling. Rising execution time suggests work inside the job or a dependency. Repeated attempts after timeouts can create both symptoms while adding duplicate side effects.
Start with actionable failure context
The following observer targets Laravel 11 on PHP 8.2 or later, matching the article’s original framework era. It records terminal failure context using Laravel’s queue failure event. Merge it into the existing provider boot method. A failed-job record does not capture every temporary exception that was retried successfully.
<?php
namespace App\Providers;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Queue::failing(function (JobFailed $event): void {
Log::error("queue.job_failed", [
"connection" => $event->connectionName,
"job_id" => $event->job->getJobId(),
"job_type" => $event->job->resolveName(),
"attempts" => $event->job->attempts(),
"exception_class" => get_class($event->exception),
]);
});
}
}
Use Horizon when the queue is Redis-backed
Horizon requires Redis queues. It is not a dashboard for an SQS or database queue connection. If the system already uses another backend, start with its queue metrics and application events rather than migrating just to obtain a dashboard.
For an existing Horizon installation, schedule its metric snapshots. The application scheduler must itself run for this entry to take effect. An empty dashboard can be a missing scheduler, not an absence of queued work.
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command("horizon:snapshot")->everyFiveMinutes();
Turn symptoms into a bounded intervention
Write a short runbook for each important workload. Include the acceptable completion window, where to find a failed job, who owns its downstream dependency, and whether retrying is safe. Payment submission and image resizing should not share the same replay assumptions.
With Laravel’s retry_after-based connections, the worker timeout must be shorter than retry_after. Otherwise another worker can receive work that is still running. Also bound network calls independently; a process timeout is not a substitute for a sensible dependency timeout. SQS visibility timeout needs its own review instead of copying a Redis setting.
Do not interpret a completion event as proof of the intended business effect. A job can return without writing its output, while a timed-out job can have completed an external request. Use a durable operation key or equivalent business-level deduplication wherever replay can create a second effect.
Verify before scaling
In a disposable test environment, enqueue a successful job, a slow job, and a job that exhausts retries. Confirm that each produces the expected observable state and that the completion record matches the intended output. A process killed abruptly may never reach a PHP event handler, so also monitor worker health and missing completions.
Change one constraint at a time: worker count, a slow query, dependency concurrency, or queue separation. Compare the same workload before and after. More workers are justified when they reduce delay without overwhelming the database or downstream service; they are not a remedy for an unbounded retry loop.

