# Migrating from Laravel Horizon

> Swap the Composer package, redeploy, restart the workers. No code changes, and no one-way door.

Source: https://boring-observability.dev/skyline/docs/migrating-from-horizon
Section: Getting started — Laravel Skyline documentation
Updated: 2026-07-13

---

Migrating from Laravel Horizon to Laravel Skyline needs **no application code changes and no migrations**. Skyline keeps the `Laravel\Horizon\` namespace and declares `replace: laravel/horizon` in Composer, so every import, gate, config key and Artisan command you already have keeps resolving — against Skyline instead of Horizon. In practice the migration is a Composer swap, a deploy, and a worker restart.

## The migration

1. Remove `laravel/horizon` from the `require` block of `composer.json`.
2. Add the Anystack registry and require Skyline — see [Installation](https://boring-observability.dev/skyline/docs/installation) for the license-key setup.
3. Run `composer update`.
4. Deploy, then restart the workers with `php artisan horizon:terminate`.

```bash
composer remove laravel/horizon
composer config repositories.skyline composer https://laravel-skyline.composer.sh
composer require boring-o11y/laravel-skyline

php artisan horizon:terminate
```

Composer will refuse to install both packages at once. That is the intended behaviour rather than a conflict to work around — `replace` is what makes every third-party package that depends on `laravel/horizon` resolve cleanly against Skyline.

## What keeps working, untouched

- Your `config/horizon.php`, copied across unchanged. Every Skyline key has a working default.
- Your `HorizonServiceProvider`, including `Gate::define('viewHorizon', …)` and `Horizon::auth()`.
- Every `use Laravel\Horizon\…` import in your application code.
- Every `php artisan horizon:*` command, by name and behaviour.
- The dashboard, still mounted at `/horizon`.
- The jobs already sitting in Redis: the payload format does not change, so nothing needs to drain first.

> **Re-publish the dashboard assets**
>
> Skyline ships its own compiled dashboard assets. If your deploy publishes Horizon's assets into `public/`, add `php artisan horizon:publish` to it — otherwise you will be serving Horizon's old JavaScript against Skyline's API and wondering why the new tabs are missing.

## Deploying without dropping jobs

The swap is an ordinary deploy, and the ordinary deploy rules apply. Workers run the code they booted with, so a worker started before the deploy keeps running Horizon's code until it is restarted — which is exactly what `horizon:terminate` is for: it lets each worker finish the job in its hands and then exit, and your process supervisor restarts it on the new code.

There is no window in which jobs are lost. A job dispatched by new code and picked up by a not-yet-restarted worker is an ordinary Redis payload either way; the two versions read and write the same format.

## Verifying the migration

After the workers come back, three things tell you the swap took:

- `composer show boring-o11y/laravel-skyline` reports the installed version.
- The dashboard at `/horizon` shows the tabbed **Jobs** view, with Scheduled and Retries as separate tabs.
- Opening a queue from the dashboard drills into the jobs waiting inside it, rather than only counting them.

If the dashboard looks exactly as it did before, you are almost certainly serving stale published assets — see the note above.

## Rolling back

Rolling back is the same operation in reverse, and it is deliberately cheap:

```bash
composer remove boring-o11y/laravel-skyline
composer require laravel/horizon

php artisan horizon:publish
php artisan horizon:terminate
```

Skyline adds **no database schema** and stores its extra tracking data under the same Redis prefix Horizon already uses. A rollback therefore loses the Skyline-only views — the trend charts, the delayed-job index, the previous-attempt panel — and leaves your queues, your jobs and your metrics history untouched. The Skyline keys left behind in Redis are trimmed on their own retention schedule and cost nothing.

> **Config keys are safe to leave in place**
>
> A `queueWeights` map or a `worker_output` key left in `config/horizon.php` after a rollback is simply ignored by upstream Horizon, which reads only the keys it knows about. You do not have to strip them out to go back.

## If the subscription lapses

Already-installed versions keep running normally — there is no runtime license check that can interrupt job processing. Only future `composer update` calls against the private registry fail authentication, until the subscription is renewed. A lapsed license is a build-time problem, never a production one.


## Common questions

### Do I have to change any code to migrate from Horizon to Skyline?

No. Skyline keeps the Laravel\Horizon\ namespace and replaces laravel/horizon in Composer, so your config/horizon.php, HorizonServiceProvider, viewHorizon gate and every Laravel\Horizon\ import keep working untouched. There are no migrations to run.

### Can I roll back from Skyline to Horizon?

Yes, and it is the same operation in reverse: require laravel/horizon again and drop the Skyline package. Skyline adds no database schema and stores its extra tracking data under the same Redis prefix, so a rollback loses the Skyline-only views and leaves your queues and jobs untouched.

### Will my queues drain or lose jobs during the migration?

No. The jobs already in Redis are untouched by the swap, because the payload format does not change. Migrating is an ordinary deploy: install the package, redeploy, then run php artisan horizon:terminate so the workers restart on the new code.
