iso-doss/laravel-meridian
Laravel Meridian is a lightweight Laravel package from iso-doss that provides a clean, configurable integration for adding Meridian functionality to your app, with a developer-friendly API, sensible defaults, and minimal setup.
Meridian is a Horizon replacement aimed at database queues, where Horizon (Redis-only) is not an option. The mental model: it listens to Laravel's queue events to write a job history table, and separately reads the jobs table to catch work that never fired an event. If you're on QUEUE_CONNECTION=database and have been debugging with select * from jobs and failed_jobs, this is the dashboard you were missing.
Minimum path:
composer require iso-doss/laravel-meridian
php artisan meridian:install # publishes config + migrations, runs them
php artisan migrate
Then visit the configured prefix (default web routes). Before anything renders you must define the gate:
// app/Providers/AppServiceProvider.php
Gate::define('viewMeridian', fn ($user) => $user?->isAdmin());
First use case: push a single job, run php artisan queue:work --queue=default, and confirm it appears in the jobs list with status transitions queued → processing → completed. If it doesn't, your meridian config doesn't list that connection as a database queue — fix that before looking anywhere else. Second use case: open the workload view and confirm your worker shows up as a heartbeat without having started Meridian's supervisor.
When you're ready to try the supervisor, do it locally first with php artisan meridian:listen. It runs Meridian and restarts it whenever a PHP file in the directories listed in the new watch config option changes — polling, so unlike horizon:listen it needs no Node and no npm packages. --poll= sets the interval in seconds; cache, vendor and storage are skipped automatically.
Separate the three planes. Meridian gives you (a) observability from events, (b) reconciliation from the jobs table, (c) process supervision. Teams get burned by conflating them. Day-to-day: use the dashboard for triage, the JSON API for alerting/automation, and the supervisor only where you'd otherwise hand-roll a supervisord block.
Supervisor in prod, meridian:listen locally. In production, run php artisan meridian under systemd or Supervisord as a single long-lived process — it forks queue:work children. Locally, use meridian:listen (auto-restart on file change, no Node toolchain) or plain queue:work if you only want heartbeats in the UI. Never run both php artisan meridian and a stray queue:work on the same queue in production; the unmanaged worker won't respond to meridian:pause / meridian:terminate and you'll chase phantom capacity.
Deploy restart is mandatory. Database workers hold code in memory. Standard deploy hook:
php artisan meridian:terminate # or queue:restart if not using the supervisor
php artisan meridian # systemd restarts it for you
Size your auto-scaling strategy to the workload. The new log strategy ('autoScalingStrategy' => 'log') distributes processes by the logarithm of each queue's pending count, so one enormous queue can't monopolise the whole pool. Use log when you have a few firehose queues alongside small ones; stick with the linear/depth-based behaviour when queue depths are broadly comparable and you want proportional capacity.
Handle the memory-limit exit code. The master supervisor now enforces memory_limit (64 MB default, 0 disables). On breach it drains running jobs and exits 12, firing MasterSupervisorOutOfMemory. Your process monitor must treat 12 as a restart, not a crash: Restart=always in systemd, autorestart=true in Supervisord. If you supervise with something that gives up on non-zero exits, your workers die quietly overnight.
Tag for searchability. Search by class/UUID is free; tags are what make the dashboard usable at volume. Attach them on the job ($this->tags = ['tenant:'.$id, 'billing']) so the jobs list is filterable by domain concern rather than fully-qualified class name.
Use silencing for known-noisy jobs. Implementing the Silenced contract (or setting $silenced / $silencedTags) moves a job out of « Completed » into its own tab without losing the record — useful for high-frequency polling or heartbeat jobs that would otherwise drown the list. Silenced jobs are retained (status silenced) until meridian:prune, and they now count in the metrics.
Cancel vs retry — know the semantics. Cancel can only remove a job that hasn't been reserved; an in-flight job will finish (or fail) regardless. Retry re-dispatches from the stored payload — for jobs with side effects, make sure the handler is idempotent. This is the same contract as Horizon, but on a database queue the payload is durable, so a retry genuinely replays the original arguments.
Tune the scheduler, don't ignore it. meridian:snapshot and meridian:prune are auto-registered, which means the throughput chart's granularity is set by snapshot cadence and table growth is set by prune retention. If schedule:run isn't actually cron'd on that host, you get an empty chart and an unbounded history table — a silent double failure. Prune retention now governs silenced rows too.
Ops API. Point your external monitor at the stats/workload endpoints rather than scraping Blade. Put real auth in meridian.route.api.middleware (auth:sanctum, an IP allowlist, whatever fits) — the gate protects the web UI, not necessarily the API surface.
Gate::define('viewMeridian', ...) in the same PR as the composer require, or every user gets 403 and it looks broken.waits previously fell back to nothing; it now uses '*', or 60 seconds when '*' isn't set, matching Horizon. Set 0 to explicitly disable a queue. Expect a wave of LongWaitDetected notifications on upgrade if your waits map was sparse — audit it before deploying, and remember notifications only fire when routes are configured.jobs table are reconciled by UUID; verify the unique index exists after migrating. Jobs from other applications sharing the database have no events, so they'll only appear after reconciliation — expect lag, and expect null wait/runtime metrics for them.queued and a processing event. Bulk-inserted and pushRaw jobs get history but sparse timing; don't build an SLO dashboard on columns that are null for half your rows.pcntl/posix requirement. php artisan meridian will not run on Windows, on minimal containers, or where those extensions are disabled. Check with php -m before promising the supervisor to anyone. The dashboard and heartbeats work fine without them.connection:queue. A typo in the key silently disables alerting — no error, no mail, no event. Verify by deliberately clogging a queue in staging and watching for LongWaitDetected.route:cache, run route:clear as part of the change or your new prefix won't exist.jobs rows; Meridian adds a history row plus transitions per job, and silenced jobs are no longer deleted on completion. At high throughput this table outgrows everything else. Watch index size, not just row count, and set separate retention expectations for completed, silenced and failed jobs.auto scaling responds to queue depth — a burst can spawn workers your DB connection pool or CPU budget can't absorb, and the log strategy only softens the skew, it doesn't impose a ceiling. Cap worker counts in config before turning on auto, and rehearse meridian:pause / meridian:continue so you know what a paused pool looks like in the UI.config/meridian.php add the new memory_limit and watch keys (defaults apply when absent), and if you published the Blade views you must republish them to get the « Silenced » tab.meridian:listen is dev-only. It polls the filesystem and restarts the whole supervisor on any watched PHP change — never point it at a production host, and keep watch narrow (skip generated directories, which it already does for cache/vendor/storage).LongWaitDetected and MasterSupervisorOutOfMemory events are the cleanest hooks for routing alerts into Slack/PagerDuty rather than relying on the built-in mail notification — the out-of-memory one is worth wiring even if you restart automatically, so you can tune the 64 MB default instead of restarting your way through a memory leak.How can I help you explore Laravel packages today?