blomstra/horizon
Integrates Laravel Horizon with Flarum so administrators can monitor Redis queues, jobs, and workers from the admin panel. Offers a convenient in-app view of queue health and failed jobs without switching to a separate Horizon dashboard.
blomstra/horizon wires Laravel Horizon — the Redis queue dashboard and supervisor — into Flarum, exposing it as an admin-only page. It is thin glue: a service provider, an auth bridge (Horizon's default gate only allows local), a route/asset mount for the Vue SPA, and (depending on version) a horizon console wrapper. You can read the entire surface in extend.php and src/ in ten minutes.
Prerequisites (non-negotiable):
ext-redis, ext-pcntl, ext-posix — Horizon will not boot without pcntl/posix, and many shared hosts block them.sync or database queues produce an empty dashboard.config.php.Install:
composer require blomstra/horizon
php flarum extension:enable blomstra-horizon
php flarum cache:clear
php flarum assets:publish
Where to look first
config.php — set queue.driver to redis (Flarum's default is sync, which is why first-time users see nothing).vendor/blomstra/horizon/extend.php — see which routes, gate and commands it registers before you debug anything.First use case: flip the queue to Redis, then trigger a job you know exists (e.g. a reply that sends an email notification, or an extension job like image processing). Watch it appear under Completed, then watch it appear under Failed after you throw an exception in it. That round trip validates the whole chain — Redis, worker, auth, dashboard — in five minutes. Metrics graphs stay empty until horizon:snapshot has run at least a couple of times (see Gotchas).
A minimal config.php queue/redis block, roughly:
'queue' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
],
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 3, // dedicated DB for queues, not the cache DB
],
Exact key names depend on your Flarum version — validate with php flarum info and confirm jobs land in Redis (redis-cli -n 3 keys 'queues:*').
Run Horizon as the worker process. Horizon replaces queue:work entirely — it starts its own supervisors from horizon.php. Under systemd/supervisor:
[program:flarum-horizon]
command=php /var/www/forum/flarum horizon
directory=/var/www/forum
autostart=true
autorestart=true
user=www-data
stopwaitsecs=3600
(php flarum horizon if the extension registers the wrapper; otherwise php /var/www/forum/vendor/bin/horizon.) Delete any existing queue:work unit in the same deploy — two competing worker pools is a classic self-inflicted incident.
Deploy hook: Horizon workers are long-lived and hold old code in memory. Every deploy needs a graceful recycle:
php flarum cache:clear
php flarum migrate --force
php flarum horizon:terminate # workers finish current job, then restart
Tag your jobs so the dashboard is searchable. Horizon reads a tags() method off the job class; without it the Completed/Failed search is nearly useless in a forum where every job looks like SendNotificationJob.
use Illuminate\Contracts\Queue\ShouldQueue;
class SendDigest implements ShouldQueue
{
public function tags(): array
{
return ['digest', 'user:'.$this->user->id];
}
}
Eloquent-backed jobs get tags for free if the model implements Illuminate\Queue\SerializesModels and you follow the QueueableEntity convention; the manual tags() above covers everything else.
Shape supervisors around Flarum's two very different workloads. Mail/notification jobs are slow and external-bound; cache/maintenance jobs are fast. Two supervisors in horizon.php beat one:
'environments' => [
'production' => [
'default' => ['connection' => 'default', 'queue' => ['default'],
'balance' => 'auto', 'processes' => 4, 'tries' => 3,
'timeout' => 60, 'maxJobs' => 500, 'memory' => 128],
'notifications' => ['connection' => 'default', 'queue' => ['notifications', 'mail'],
'balance' => 'simple', 'processes' => 2, 'tries' => 5,
'timeout' => 120, 'maxJobs' => 250, 'memory' => 192],
],
],
Day-to-day monitoring workflow: Metrics tab → is wait time climbing (under-provisioned) or flat with low throughput (Redis/network bottleneck)? Failed tab → read the exception, use Retry after a fix. Completed tab → search by tag (user:1234) to trace one user's jobs. Pending → backlog during a mail storm. Use Pause on a supervisor before a risky migration, Continue afterwards, Purge to clear a broken queue.
Scheduling the snapshot. Horizon's metrics come from horizon:snapshot, which must run every ~5 minutes. Flarum schedules through php flarum schedule:run in cron; check whether the extension registered it (php flarum schedule:list or inspect the container's scheduler). If not, add it yourself in a tiny extension or system cron:
*/5 * * * * cd /var/www/forum && php flarum horizon:snapshot >> /dev/null 2>&1
Extension point for non-admin access. Horizon gates access through Horizon::auth(). If you need a support role (not full admin) to see the dashboard, register your own gate in a small extension's service provider rather than patching the package — the extension already relies on that same hook.
queue.driver is redis and that the connection name in queue.connection matches a Redis block. sync executes jobs inline and never touches Redis, so Horizon has literally nothing to show.maxmemory-policy is allkeys-lru, Redis will silently evict queued jobs. Give queues their own database (or instance) with noeviction./horizon/api/* calls return 403, you're either not an admin or another extension re-registered the gate. Check the network tab — this is a two-minute diagnosis, don't reinstall first./forum and reverse proxies that strip prefixes routinely break asset and API URLs. Same failure class: stale compiled assets — run php flarum assets:publish and hard-reload before filing a bug.horizon:terminate is not optional. Workers keep the old code after a deploy. If a "fixed" job still fails with the old stack trace, you forgot to terminate.timeout must be lower than the job's $timeout and PHP's max_execution_time; otherwise jobs get MaxAttemptsExceededException while still genuinely running, then execute twice.maxJobs in the few hundreds and memory below memory_limit; a worker that dies silently looks like "jobs just stopped" in the dashboard.pcntl/posix missing kills the command at startup with a cryptic message. php flarum horizon:status from the CLI is the fastest way to confirm the environment, not to debug via the web UI.composer update can pull a Horizon that conflicts with (or is untested against) your Flarum branch. Pin it in composer.json and re-verify in staging on every Flarum upgrade.php flarum horizon:clear (or purge from the UI) so a bad week of mail jobs doesn't bloat the DB.extend.php will usually tell you whether to fork the 200-odd lines or drop it and expose stock Horizon behind a small custom Flarum provider with your own Horizon::auth() gate.How can I help you explore Laravel packages today?