# Laravel Apex: de queue-worker-supervisor

13 november 2026 · \[note\] · 5 min lezen

Horizon is het juiste antwoord voor één app met constant verkeer. Wij hadden iets anders nodig: veel installaties op één server, de meeste het grootste deel van de tijd idle.

[\#laravel](/nl/blog?tag=laravel) · [\#queues](/nl/blog?tag=queues) · [\#open-source](/nl/blog?tag=open-source)

Apex is een package van [Symphoria](https://symphoria.io), afgesplitst van de supervisor die de installaties van Symphoria zelf draait. Voor één Laravel-applicatie met constant verkeer is [Horizon](https://laravel.com/docs/horizon) het makkelijke antwoord: first-party, een dashboard, en iedereen kent het al. Onze situatie is anders: veel installaties op één machine, de meeste het grootste deel van de tijd idle.

## Waar Horizon niet meer past

Horizon draait minimaal één proces per queue en kent de machine als geheel niet. Twintig idle installaties kosten twintig keer een worker die niets te doen heeft. En als ze wakker worden, starten ze allemaal tegelijk op.

Dat laatste is het echte probleem. Een queue die van 0 naar 8 workers gaat, betekent acht keer tegelijk het framework opstarten. Doen meerdere installaties dat samen, dan ligt de server plat.

## Wat Apex anders doet

Apex is een supervisor, geen queue-backend. Jobs blijven op Laravels `redis`- of `database`-driver; Apex leest de achterstand uit de queue en bepaalt hoeveel workers elke queue nodig heeft.

- **De vloer is nul.** Een queue met `min_processes: 0` draait geen workers als er niets in de wachtrij staat. Wat overblijft is één masterproces per installatie.
- **Een warme pool zolang er mensen zijn.** `min_processes_when_active` houdt workers klaar zolang er menselijk verkeer is, zodat gebruikers nooit op een koude start wachten, en nachten en weekenden kosten niets. Polling van het dashboard telt niet als activiteit, zodat het monitoringscherm niet zelf de load veroorzaakt die het laat zien.
- **Opstarten wordt globaal gedoseerd.** Maximaal `max_concurrent_boots` workers starten tegelijk op, met `boot_spawn_interval_ms` ertussen.
- **Direct omhoog, langzaam omlaag.** Opschalen gebeurt meteen; afschalen wacht een debounce af, zodat een korte dip geen kill/spawn-cyclus start.
- **Bursten wijkt voor de CPU.** Een queue die druk blijft mag boven zijn maximum, één worker per tick, tenzij de CPU al boven `cpu_max_percent` zit.

## Keuzes die vreemd lijken

- **Geen dashboard.** Apex levert een JSON-API en geen scherm. Het package beheert de data en de acties; de applicatie bepaalt de UI.
- **Geen `file`-store.** Dat betekent zelf locking schrijven, en dat faalt anders op Windows en ongemerkt op netwerkshares. Voor "één bestand, geen daemon": SQLite met WAL.
- **Eén master per installatie.** Een tweede `apex:start` weigert en meldt welke host en pid de lock hebben. Twee masters zien elkaars workers niet en zouden dezelfde queues voorbij elk maximum schalen.
- **Migraties worden geladen, niet gepubliceerd.** Publiceren maakt je eigenaar van het schema, en latere kolomtoevoegingen bereiken je dan nooit meer.

## Hoe Symphoria het configureert

De config van Symphoria zelf volgt één regel: één queue per soort wachten, niet per module. Twee feiten bepalen de vorm. Een worker kost een volledige framework-boot, dus een pool blijft alleen warm waar iemand die boot merkt. En een job die op een andere job wacht, moet op een andere queue staan dan de job waarop hij wacht.

- **Chat blijft warm.** `chat` houdt één worker aan, ook als er niemand is, plus een standby zolang er mensen actief zijn: er kijkt iemand naar het antwoord.
- **Tool calls krijgen een eigen queue.** Een chatrun waaiert tool calls uit en wacht erop. Op `chat` zou de wachtende run precies de worker blokkeren die zijn tool nodig heeft, dus `tools` staat apart en houdt altijd één worker warm.
- **Batches bursten, voorzichtig.** Een map importeren zet honderden jobs tegelijk klaar en daarna urenlang niets. `ingestion` draait maximaal twee workers en voegt pas helpers toe als de achterstand 15 seconden blijft staan; die helpers wachten op de CPU, draaien met lage prioriteit en leven kort.
- **Watchdogs staan nooit achter het werk dat ze redden.** Ze krijgen een eigen kleine queue; op `default` zou een watchdog pas na de schade aankomen.
- **Onderhoud schaalt naar nul.** Achtergrondqueues draaien helemaal geen workers als er niets klaarstaat.

```php
'tools' => [
    'profile' => 'latency-critical',
    'group' => 'chat',
    'min_processes' => 1,
    'min_processes_when_active' => 2,
    'max_processes' => 8,
    'use_activity' => true,
    'timeout_seconds' => 60,
    'burst_threshold_depth' => 4,
    'burst_threshold_sustained_ms' => 1000,
    'burst_max_extra' => 4,
],
```

## Lessen in de config

- **Timeouts hebben een volgorde.** De `timeout_seconds` van elke queue ligt op of boven de langste `$timeout` van zijn jobs, en `retry_after` in `config/queue.php` ligt daar weer boven. Anders wordt een trage job twee keer uitgedeeld.
- **Deploys wachten op antwoorden.** Een herstart stopt workers netjes en killt wat na een wachttijd nog bezig is. Die wachttijd moet de langste interactieve job dekken, anders breekt elke deploy lopende antwoorden af. De master stopt zodra de laatste worker weg is, dus een idle installatie herstart nog steeds binnen een seconde.
- **Idle betekent stil.** Vier keer per seconde pollen zonder workers, zonder achterstand en zonder gebruikers was het grootste deel van het Redis-verkeer over alle installaties. Nu kijkt een idle master elke vijf seconden, en zodra er iets verandert gaat hij de volgende tick terug naar het normale tempo.
- **Standaard geen OPcache in workers.** Het scheelt zo'n 170 ms boottijd, maar CLI-OPcache wordt niet gedeeld tussen processen: elke worker kost 30 tot 45 MB extra, en met `validate_timestamps=0` kan een verse worker na een deploy oude code draaien.

## Wanneer gebruik je het

Eén app met constant verkeer: blijf bij Horizon. Met Apex lever je het dashboard en first-party support in. Apex is voor de situatie waarin veel installaties een server delen en idle workers de grootste kostenpost zijn.

Symphoria publiceert de code op [GitHub](https://github.com/symphoria-io/laravel-apex) onder de MIT-licentie: `composer require symphoria/laravel-apex`.

[Ouder: Van software developers naar software architecten](/nl/blog/from-developers-to-architects) · [Alle posts](/nl/blog) · [Project: Laravel Apex](/nl/projects#laravel-apex) · [RSS-feed](/nl/feed.xml)
