Laravel Horizon — queue monitoring voor bedrijfskritische processen
Terug naar blog

Laravel Horizon — queue monitoring voor bedrijfskritische processen

AuthorRuthger Idema
22 juni 20269 min leestijd

Laravel Horizon bewaakt Redis-queues in e-commerce — van supervisor-configuratie en auto-balancing tot alerting op failed jobs en queue depth bij ERP-koppelingen.

Laravel Horizon — queue monitoring voor bedrijfskritische processen

Een mislukte queue-job om 03:14 uur. Een ERP-integratie die stilligt. Honderden orders die niet gesynchroniseerd worden. Pas om 08:00 uur ziet iemand het. Dat kost klanten, dat kost omzet, en dat kost vertrouwen. Met Laravel Horizon heb je dit soort situaties structureel in beeld — en pak je ze aan voordat de telefoon rinkelt.

Laravel Horizon is de officiële queue-dashboard en configuratietool voor Redis-gebaseerde queues. Niet alleen een mooi dashboard, maar ook de plek waar je balancing, prioriteiten en alerting centraal beheert. Voor e-commerce — met koppelingen naar ERP-systemen, externe fulfillmentpartijen of betaalproviders — is dat geen luxe.


Waarom Redis en niet de database-driver

De standaard Laravel queue-driver is de database-driver. Die werkt. Maar bij meer dan een paar honderd jobs per minuut loop je al snel tegen performanceproblemen aan: database locks, trage polling, en queries die je andere transacties vertragen. De jobs-tabel groeit, indexes worden zwaar, en je merkt het juist op de momenten dat het er toe doet.

Redis is een in-memory datastore. Een job pushen kost microseconden. Horizon is exclusief gebouwd voor Redis en levert daar bovenop metrics, realtime-monitoring en supervisor-configuratie.

Wij zien bij klanten dat de overstap van database-driver naar Redis + Horizon de queue-verwerkingstijd met 60-80% verlaagt bij piekbelasting — zoals een flash sale of een bulkimport van 10.000 productregels. Het verschil is het grootst bij jobs die snel achter elkaar worden gedispatcht: productfeed-updates, order-events, of batch-exports.

Wanneer is Redis-queue NIET de juiste keuze? Bij hele kleine projecten zonder integratiedruk, of als je hosting Redis niet ondersteunt. In dat geval volstaat de database-driver prima. En voor pure debugging in development is Laravel Telescope een betere keuze dan Horizon — die twee vullen elkaar aan maar overlappen qua doel.

Horizon installeren en configureren

bash
composer require laravel/horizon
php artisan horizon:install
php artisan migrate

Na installatie staat er een config/horizon.php. De kern van die configuratie is het environments-blok:

php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection'  => 'redis',
            'queue'       => ['critical', 'orders', 'default'],
            'balance'     => 'auto',
            'minProcesses' => 2,
            'maxProcesses' => 20,
            'tries'       => 3,
            'timeout'     => 90,
        ],
    ],

    'local' => [
        'supervisor-1' => [
            'connection'  => 'redis',
            'queue'       => ['default'],
            'balance'     => 'simple',
            'processes'   => 3,
            'tries'       => 1,
        ],
    ],
],

Drie dingen die hier tellen:

  • queue-volgorde: de array bepaalt de prioriteit. critical wordt altijd eerst verwerkt.
  • balance: auto laat Horizon zelf workers verdelen op basis van de queue-belasting. simple verdeelt gelijkmatig. false zet balancing uit.
  • minProcesses / maxProcesses: Horizon schaalt automatisch op en af tussen deze grenzen.

Supervisors en balancing in de praktijk

Een supervisor is een groep workers die Horizon beheert. Je kunt meerdere supervisors definiëren — nuttig als je queues hebt met verschillende SLA's.

php
'supervisor-1' => [
    'queue'       => ['critical', 'orders'],
    'balance'     => 'auto',
    'maxProcesses' => 15,
],
'supervisor-2' => [
    'queue'       => ['emails', 'exports'],
    'balance'     => 'auto',
    'maxProcesses' => 5,
],

Supervisor-1 heeft meer capaciteit voor business-kritische jobs (orders, ERP-sync). Supervisor-2 verwerkt lagere prioriteiten zonder concurrentie om dezelfde workers.

In de praktijk: stel maxProcesses niet te laag in. Horizon schroeft workers terug als queues leeg zijn, maar bij plotselinge piek (importbatch, midnacht restock) wil je dat er direct capaciteit beschikbaar is. Reken voor ERP-koppelingen met minimaal 4-8 workers op de kritische queue.


Metrics en wat je ermee doet

Horizon verzamelt automatisch:

MetricWat het zegtActie bij afwijking
Throughput (jobs/min)Verwerkingscapaciteit van je workersTe laag? Schaal workers op
Runtime (gemiddeld)Gemiddelde uitvoertijd per job-typeStijgt? Externe API vertraagt
Queue depthHoeveel jobs wachten op verwerkingGroeit? Onvoldoende capaciteit
Failed jobsJobs die na alle retries zijn misluktElk geval is een incident
Wait timeHoe lang een job gemiddeld in de queue staatBoven 10s? Urgente actie

De combinatie van queue depth en wait time is de meest directe indicator voor problemen. Queue depth groeit? Dan zijn er te weinig workers, is een externe API traag, of is er een deadlock. Wait time schiet omhoog terwijl queue depth laag blijft? Dan zijn individuele jobs traag — zoek naar timeouts of blocking calls.

Wij richten bij klanten standaard een aparte monitoring-job in die elk uur de queue-diepte én de gemiddelde wait time logt naar de applicatie-log. Dat geeft historische data — handig bij incidentanalyse achteraf. Combineer dat met een tool als BetterStack (voorheen Logtail) of Datadog voor grafische weergave over tijd.


Alerting op failed jobs en queue depth

Horizon heeft ingebouwde notificaties via Horizon::routeSmsNotificationsTo() en Horizon::routeMailNotificationsTo(). Maar voor serieuze alerting gebruik je de event-listeners.

Failed job alerting:
php
// In AppServiceProvider::boot()
Queue::failing(function (JobFailed $event) {
    // Stuur Slack-melding, PagerDuty, of eigen monitoring
    Notification::route('slack', config('services.slack.webhook'))
        ->notify(new QueueJobFailed($event->job->resolveName(), $event->exception));
});
Queue depth alerting via scheduled check:
php
// In App\Console\Kernel of via een Artisan-command
$depth = Queue::size('orders');

if ($depth > 500) {
    Notification::route('slack', config('services.slack.webhook'))
        ->notify(new QueueDepthAlert('orders', $depth));
}

Plan deze check elke 2-5 minuten via php artisan schedule:run. Drempelwaarden zijn projectspecifiek — voor een ERP-integratie met hoge orderdoorvoer is 500 al een signaal; voor een kleine webshop pas bij 100.

Horizon's eigen notificaties via mail zijn handig voor ontwikkelaars, maar voor operationele alerting is een Slack-webhook of integratie met tools als BetterUptime, Pagerduty of OpsGenie betrouwbaarder.

ERP en order-integraties: de harde use cases

De meeste Laravel e-commerce projecten hebben een of meer van deze integraties:

  • Ordersynchronisatie naar ERP (Exact, AFAS, SAP)
  • Voorraadmutaties terug naar het webplatform
  • Betalingsbevestigingen vanuit PSP
  • Verzendstatus-updates van fulfillmentpartijen
  • Productdata-import vanuit PIM of inkoopsysteem

Al deze koppelingen verlopen via queue-jobs. En al deze koppelingen falen weleens — door een API-timeout, een netwerkhiccup, een ongeldige response, of een certificaat dat verlopen is. Dat is normaal. Wat niet normaal is: er pas twee uur later achter komen. Of erger: het helemaal niet merken totdat een klant belt.

Het patroon dat wij het vaakst zien: een externe API is traag (>30s response), de job raakt de timeout, faalt na vijf retries, en verdwijnt in de failed-jobs-tabel. Ondertussen loopt de queue-diepte op omdat nieuwe jobs dezelfde trage API aanroepen. Zonder monitoring is dit onzichtbaar.

Concrete inrichting voor ERP-sync:
php
class SyncOrderToErp implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public int $backoff = 60; // 60 seconden tussen retries
    public int $timeout = 120;

    public function handle(ErpClient $client): void
    {
        $client->syncOrder($this->order);
    }

    public function failed(Throwable $exception): void
    {
        // Stuur een notificatie naar het operations-team
        Notification::send(
            User::role('operations')->get(),
            new OrderSyncFailed($this->order, $exception)
        );
    }
}

De failed()-methode is de vangnet-laag. Na 5 pogingen (met exponentieel backoff) weet het operations-team het. Orders worden dan handmatig verwerkt, maar de informatie is er.


Horizon in productie draaien

Horizon draait als een apart proces naast je webserver. In productie gebruik je Supervisor (het Linux-daemon-tool, niet verwarren met Horizon's supervisor-concept). Op een Laravel Forge-server is dit al geconfigureerd als je Horizon via Forge installeert. Op een zelf beheerde server maak je een configuratiebestand aan:

ini
[program:horizon]
process_name=%(program_name)s
command=php /home/forge/coding.nl/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/home/forge/coding.nl/storage/logs/horizon.log
stopwaitsecs=3600
stopwaitsecs=3600 geeft Horizon een uur om lopende jobs af te ronden bij een graceful shutdown. Dit is van belang als je lange-termijn jobs hebt — een grote exportbatch die 20 minuten draait wil je niet halverwege afbreken.

Bij deploys doe je:

bash
php artisan horizon:terminate

Horizon stopt na de huidige jobs, waarna Supervisor het opnieuw opstart met de nieuwe code. Zo verlies je geen jobs bij een deploy. Voeg dit toe aan je deploy-script, na de php artisan config:cache en php artisan migrate --force stappen.


Dashboard-toegang beveiligen

Standaard is het Horizon-dashboard alleen toegankelijk in local. In productie voeg je een gate toe in HorizonServiceProvider:

php
protected function gate(): void
{
    Gate::define('viewHorizon', function ($user) {
        return in_array($user->email, [
            '[email protected]',
        ]);
    });
}

Koppel dit aan je bestaande authenticatielaag. Stel het dashboard nooit publiek beschikbaar — het toont interne job-data inclusief payloads.


Horizon versus alternatieven

Horizon is niet de enige optie. Voor context:

OptieGeschikt voorNadeel
Laravel HorizonRedis-queues, medium/large scaleAlleen Redis
Laravel TelescopeDebugging in developmentNiet voor productie-monitoring
Database-driver + cron-checkKleine projectenSchaalt slecht bij volume
AWS SQS + CloudWatchCloud-native setupsGeen ingebouwde Laravel UI
Soketi + ReverbRealtime events (niet queues)Ander use case

Voor een Laravel e-commerce project met ERP-koppelingen en dagelijks ordervolume boven de 100-200 stuks: Horizon is de logische keuze.


Kosten en infrastructuur

Horizon zelf is gratis (MIT-licentie). De kosten zitten in Redis:

  • Managed Redis (DigitalOcean, Upstash, Redis Cloud): reken op 10-50 euro/maand afhankelijk van schaal
  • Self-hosted Redis op je eigen VPS: geen extra kosten, maar eigen beheer
  • Redis Cluster voor hoge beschikbaarheid: pas relevant boven ~1.000 jobs/minuut

Voor de meeste Shopify- of Magento-integraties in een Laravel microservice is een managed Redis-instance van 15-25 euro/maand ruim voldoende.


Veelgestelde vragen

Kan ik Horizon gebruiken zonder bestaande queue-setup te herschrijven?

Ja. Zolang je jobs ShouldQueue implementeren en je queue-driver naar Redis wijst, werkt Horizon direct. Je hoeft bestaande jobs niet te herschrijven — je voegt alleen config/horizon.php toe en start het proces.

Wat gebeurt er als Horizon crasht?

Jobs blijven in Redis staan. Horizon verliest geen data bij een crash. Supervisor herstart het proces automatisch. Jobs die middenin de verwerking zaten, worden bij herstart opnieuw opgepakt (tenzij ze als unique zijn gemarkeerd).

Hoeveel workers heb ik nodig voor een ERP-integratie?

Dat hangt af van de job-runtime en het volume. Als een ERP-sync gemiddeld 2 seconden duurt en je verwerkt 300 orders per uur, heb je minimaal 1 worker nodig — maar door pieken en latency reken je beter op 4-6 workers op de orders-queue. Monitor de wait time in Horizon en schaal op als die structureel boven 5-10 seconden zit.

Moeten failed jobs altijd handmatig worden herstart?

Nee. Met php artisan queue:retry all herstart je alle mislukte jobs in bulk. Je kunt dit ook automatiseren na een externe API-storing: zodra de storing voorbij is, trigger je een retry via een Artisan-command in je deployment-pipeline of via een scheduled check. Wees wel voorzichtig met blinde bulk-retries — zorg dat de onderliggende oorzaak opgelost is.


Horizon draaien op een productiesysteem is geen groot project. Een middag werk, en je hebt realtime inzicht in je queue-gezondheid, directe alerting bij failures, en historische data voor postmortems. Benieuwd hoe dit past in jouw Laravel-stack? Neem contact op — we kijken graag mee.

Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check