Skyline

Job lifecycle logging

Log every job transition — including the silent unique-lock discards.

Skyline can log every transition a queued job makes — queued, reserved, released, migrated, retried, timed out, completed, failed — to a log channel you choose, with each line tagged by job id so one job's whole history is a single grep. It also logs the two transitions nothing else reports: a dispatch discarded because a ShouldBeUnique lock was held, and a job dropped by a WithoutOverlapping lock.

That last part is the reason the feature exists. A queued job passes through half a dozen states and Laravel logs almost none of them — and some transitions are not observable at all, because the framework emits no event for them.

Enabling#

Point log_channel at the channel that should receive the lines:

// config/horizon.php
'log_channel' => env('HORIZON_LOG_CHANNEL'),
# .env
HORIZON_LOG_CHANNEL=queue

Leave it unset to use the application's default channel. If the configured channel name does not resolve, Skyline falls back to the default channel rather than throwing out of a queue listener — a logging misconfiguration should never take down job processing.

Log level is the volume dial#

Every event is emitted at a level chosen to match its operational severity, so you tune coverage purely by setting the level on the channel — no separate on/off switches.

  • error — terminal failures only.
  • warning — failures, timeouts, and the silent drops: unique-lock discards and WithoutOverlapping drops.
  • info — the above, plus releases, migrations and retries.
  • debug — everything, including a pending / reserved / completed line for every successful job.
Mind the volume at debug

At debug, a healthy queue writes three lines per job. On a queue doing 120,000 jobs a day that is 360,000 lines a day. Use debug to trace a problem, not as a steady state.

The events#

Event Level When
job.pending debug Job was queued. Notes the delay, when delayed.
job.reserved debug A worker picked the job up and began processing.
job.completed debug Job finished successfully.
job.migrated info A delayed job became available and moved onto the ready queue.
job.released info
warning if the reason is an exception
Job went back onto the queue, with the reason and the delay before it runs again.
job.retried info A failed job was retried, recording the new job's id.
job.timed_out warning Job exceeded its timeout, noting whether it will be retried or was marked failed.
job.overlap_dropped warning Job was dropped without releasing because a WithoutOverlapping lock was held and the middleware is set to dontRelease().
unique_lock.discarded warning A dispatch was skipped because a ShouldBeUnique lock was already held. The job was never queued.
job.failed error Job failed terminally and will not be retried.

Lines read like this, with the job id in the message so a plain text search finds it:

[job:8813] queued onto [default] (delayed 30s)
[job:8813] migrated to [default] and is ready to run.
[job:8813] reserved from [default] and started processing.
[job:8813] released back to [default] (reason=exception, delay=15s)
[job:8813] failed on [default] and will not be retried (RuntimeException).

Structured context#

Every line also carries a structured context array, which is what you actually query once the lines are in a log pipeline. All events include event, job_id, job, connection and queue. Beyond that:

Event Additional context
job.released reason, delay
job.timed_out will_retry
job.failed exception, message
job.retried retry_id
unique_lock.discarded unique_id, unique_lock_key, job_hash
job.overlap_dropped overlap_lock_key

Why a job was released#

"Released back to the queue" is the single most ambiguous state in a Laravel queue: it can mean the job threw, that it hit an overlap lock, or that it called $job->release() itself. Skyline attributes it, so job.released carries a reason of exception, without_overlapping, or released (a manual release).

To distinguish overlap drops from manual releases

Import Skyline's drop-in middleware instead of the framework's — it is otherwise identical:

use Laravel\Horizon\Middleware\WithoutOverlapping;

Without it, a job released by an overlap lock is reported with the generic released reason, and job.overlap_dropped is never emitted.

Unique-lock discard caveat#

Discard logging works by instrumenting the lock the framework takes at dispatch, and it covers the application's default cache store. A job that overrides uniqueVia() to lock on a different store is not covered — there is no framework event for the silent drop, so there is nothing to hook. The same applies if you have replaced the default cache repository with a custom subclass.

For the closely related problem of a lock that is never released, see Unique job locks.

Common questions

How do I log Laravel queue job lifecycle events?

Point log_channel in config/horizon.php at the channel that should receive the lines (for example HORIZON_LOG_CHANNEL=queue), or leave it unset to use the application default channel. Every line is tagged with the job id, so a single job's whole history is one grep away.

How do I control the volume of job lifecycle logs?

The channel's own log level is the volume dial, because each event is emitted at a level matching its operational severity: error is terminal failures only; warning adds timeouts and the silent drops; info adds releases, migrations and retries; debug logs a line for every successful job too. At debug a healthy queue writes three lines per job, so use it to trace a problem rather than as a steady state.

Why was my job released back to the queue?

Skyline attributes it: the job.released event carries a reason of exception, without_overlapping, or released (a manual $job->release()). To distinguish overlap drops from manual releases, import Skyline's drop-in Laravel\Horizon\Middleware\WithoutOverlapping instead of the framework's — it is otherwise identical.

Queue control, not just queue monitoring.

Skyline is a drop-in replacement for Laravel Horizon that lets you act on what you see — pause a queue, jump a job to the front, drain a backlog. $99 once, every app you run it on.

Buy Skyline — $99 once

Secure checkout by Anystack. 30 days to change your mind.