Skyline

Troubleshooting

The errors people actually hit, and what each one is telling you.

The problems people actually hit with Laravel Skyline, and what each one is telling you. Most of them are authentication against the private Composer registry, or a feature quietly declining to run because its requirements are not met — Skyline prefers to say so, with a status code or a banner, rather than pretend.

Composer returns 401 for laravel-skyline.composer.sh#

The machine running Composer has no valid credentials for the registry. The username is the licensee email tied to the purchase and the license key is the password — a common cause is putting the key in both fields.

composer config --global --auth http-basic.laravel-skyline.composer.sh \
  you@example.com \
  YOUR-LICENSE-KEY

Every machine that pulls the package needs this, not just your laptop: CI runners, build containers and deploy hosts included. On those, prefer the COMPOSER_AUTH environment variable over a committed auth.json:

export COMPOSER_AUTH='{"http-basic":{"laravel-skyline.composer.sh":{"username":"you@example.com","password":"YOUR-LICENSE-KEY"}}}'

If the subscription has lapsed, this is also what you will see — and only here. Already-installed versions keep running; a lapsed license is a build-time failure, never a runtime one.

Dependabot's pull requests fail to install#

Almost always the Actions/Dependabot secret split. ${{ secrets.* }} inside .github/dependabot.yml resolves against Dependabot secrets, stored under Settings → Secrets and variables → Dependabot. A secret of the same name under Actions is invisible there, which is why an otherwise-correct config still 401s.

The same split catches the CI runs on the pull requests Dependabot opens: those workflow runs are given the Dependabot secrets, not the Actions ones. If your build runs composer install, the credentials it reads must also exist as Dependabot secrets — otherwise Skyline's own update PRs will be the only ones whose CI fails.

And if Dependabot skips your Composer manifest entirely rather than failing on one package, check that the update entry names the registry: without registries: [skyline] it resolves boring-o11y/laravel-skyline against Packagist, does not find it, and gives up on the whole file. See Installation.

Composer refuses to install Skyline alongside laravel/horizon#

That is intended. Skyline declares replace: laravel/horizon, which is what makes every package that depends on Horizon resolve against Skyline instead. The two cannot coexist. Remove laravel/horizon from your require block — see Migrating from Laravel Horizon.

Pausing a queue returns 409 Conflict#

Per-queue pausing is built on Laravel's native queue-pause API and needs a shared cache store. Skyline responds 409 — and hides the per-queue Pause buttons behind an explanatory banner — when either requirement is unmet:

  • The framework does not provide the queue-pause API. It needs a recent enough Laravel; on older versions the feature hides itself rather than half-working.
  • The cache store is array or null. Neither can carry state between the dashboard process and the worker processes, so a pause set in one would be invisible to the other. They are rejected rather than silently dropped.

Global and per-supervisor pause (horizon:pause, horizon:pause-supervisor) work regardless, because they signal the master process rather than storing state. See Pausing & resuming queues.

The metrics page is empty#

horizon:snapshot is not scheduled. The per-job and per-queue metrics tables are driven by Horizon's snapshot command exactly as upstream, and without it there is nothing to draw:

use Illuminate\Support\Facades\Schedule;

Schedule::command('horizon:snapshot')->everyFiveMinutes();

This catches people migrating from a Horizon install that never had it either — the metrics page was empty before the swap too, and Skyline is simply the first thing that made you look.

Workload and wait samples are taken from the php artisan horizon master supervisor loop, not from a scheduled command. A gap in the chart means the master process was not running for that period — a deploy, a crashed supervisor, a scaled-to-zero worker dyno. Nothing else is affected, and the series resumes on its own when the master comes back.

If the charts are empty rather than gappy, check that the retention window has not outrun the data: with the default trends.interval of 15 minutes, a freshly started process has one bucket to draw.

A ShouldBeUnique job has silently stopped dispatching#

Its unique lock is almost certainly still held. If the job was ever deleted straight out of Redis — from a dashboard, or by horizon:clear — on a version of Horizon that did not release the lock, then nothing ever released it, and uniqueFor defaults to 0, meaning no expiry. Every dispatch since has been discarded without a word.

Skyline releases the lock on all three out-of-band paths, so it will not happen again. Clearing an already-wedged lock means deleting the lock key from your cache store, and the fastest way to find it is to turn on discard logging — Skyline logs every dispatch dropped because a lock was held, which the framework itself emits no event for. See Unique job locks.

The dashboard looks exactly like Horizon's#

You are serving stale published assets. Skyline ships its own compiled dashboard assets; if your deploy publishes Horizon's into public/, it needs to re-run the publish step after the swap:

php artisan horizon:publish

The tell is a dashboard that works but has no Scheduled and Retries tabs: old JavaScript, talking to Skyline's API.

onFront() appears to do nothing#

Three possibilities, in the order they are usually the cause:

  • The job class is missing the trait. PendingDispatch is not macroable, so onFront() reaches the job through its __call proxy — without use InteractsWithFrontOfQueue on the job, the call has nowhere to land.
  • The job is delayed. A ->delay(...) job lives in a sorted set ordered by its available-at time, not on the ready list, so there is no head to jump to yet.
  • The connection is not Redis. The behaviour relies on Redis list semantics and does not apply to the sync or database drivers.

See Front-of-queue dispatching.

Provisioning throws about queueWeights and balance#

The [supervisor-1.queueWeights] option only applies when [balance] is false;
each queue gets its own pool when balancing.

Weights order the queues a single worker checks. Under simple or auto balancing each queue already gets its own process pool, so there is no ordering left to weight and the two settings contradict each other. Skyline throws rather than silently ignoring one of them. Either set 'balance' => false on that supervisor, or drop the queueWeights map. See Weighted queues.

Common questions

Why does Composer return 401 for laravel-skyline.composer.sh?

The machine running Composer has no valid credentials. The username is the licensee email tied to the purchase and the password is the license key. On CI, pass them through COMPOSER_AUTH; for Dependabot, store them as Dependabot secrets, which are a different store from Actions secrets.

Why does pausing a queue return 409 Conflict?

Per-queue pausing needs Laravel's native queue-pause API and a shared cache store. On a framework version without that API, or with the array or null cache store, Skyline responds 409 rather than pretending to pause. Global and per-supervisor pause are unaffected.

Why is my metrics page empty?

horizon:snapshot is not scheduled. Add Schedule::command('horizon:snapshot')->everyFiveMinutes() to routes/console.php. This is upstream Horizon behaviour rather than anything Skyline changes.

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.

Sign up for early access