← Blog

Process automation

Self-hosted n8n: hoe een stale sentinel 11.400 events at

Op maandagochtend lagen er 47 woedende tickets in de inbox. De kandidatenpipeline was leeg. Het queue dashboard stond op gezond. De 11.400 missende events waren al weg.

Jacob Molkenboer· Oprichter · A Brand New Company· 10 jun 2026· 9 min
Omgevallen messing relais, gerafelde pneumatische slang met stof, gescheurd papieren logboek met groen tabje, gebroken rode lakzegel.

Het eerste ticket kwam binnen om 08:14 op maandag. Een recruiter bij een ziekenhuisklant in Eindhoven kon de zeven sollicitaties niet zien die in het weekend op haar ATS waren binnengekomen. Om 09:30 stonden er zevenenveertig tickets open. De kandidatenpipeline was leeg. Het n8n queue dashboard stond op gezond. De webhook receiver gaf netjes 200's op elke test payload die de support engineer er tegenaan gooide.

De events waren niet vertraagd. Ze waren weg. Elfduizend vierhonderd, verspreid over tweeënzeventig uur.

Dit is wat er gebeurde binnen een HR-tech SaaS van 33 mensen in Apeldoorn tussen vrijdagavond en maandagochtend, en de post-mortem die we de dinsdag erna samen met hun lead engineer schreven.

De stack op vrijdag

De klant draait een workforce platform dat tussen vacaturesites, ATS-systemen en payroll zit. Het meeste inkomende werk bestaat uit webhooks: een nieuwe sollicitatie uit Greenhouse, een statuswijziging uit BambooHR, een contractevent uit Visma, een Slack-notificatie van interne recruiters. Ongeveer 4.000 tot 6.000 events op een doordeweekse dag, minder in het weekend, maar nooit nul. Ziekenhuizen en magazijnen plaatsen elke dag vacatures.

Ze draaien self-hosted n8n in queue mode: één main instance die webhooks en de editor afhandelt, drie worker instances die jobs ophalen uit een Redis-backed BullMQ. Postgres bevat de workflow-definities en de execution history. Redis draait als een drie-node Sentinel setup op hetzelfde Hetzner-cluster, opgezet nadat een single-node Redis het platform eind 2024 negentig minuten lang plat had gelegd.

Het is een verstandige architectuur. Het is ook precies de architectuur die je in zo'n tweehonderd middelgrote Europese SaaS-teams aantreft die de afgelopen achttien maanden voor n8n kozen in plaats van Zapier. Niets exotisch aan.

De storing op vrijdag om 19:42

De Redis master node liep tegen een kernel OOM aan en werd gekilled. De Sentinels promoveerden binnen vier seconden een replica. Tot zover gezond. De main n8n instance merkte het op, opende binnen dertig seconden een nieuwe verbinding naar de gepromoveerde master en begon weer webhooks te accepteren. De Sentinel logs zijn schoon. redis-cli -p 26379 sentinel masters liet de nieuwe master zien met de juiste flags.

De workers merkten het niet.

Twee van de drie workers waren gestart met een hardcoded Redis host in hun env-bestand, niet met een Sentinel client config. De derde worker had het Sentinel-blok wel, maar had het oorspronkelijke master IP gecached in zijn connection pool en de sentinel-topologie nooit ververst. Toen de master stierf, hielden alle drie de workers verbindingen open die elke paar seconden stilletjes timed-out raakten, het opnieuw probeerden bij een IP dat niet meer reageerde, en niets logden boven WARN.

De main instance van n8n bleef rustig jobs enqueueën. BullMQ maakt het niet uit dat geen enkele worker iets ophaalt. De waiting list groeide. Er ging niets verloren. Nog niet.

De retry queue die de events opat

Het echte verlies kwam door een setting die we dit jaar bij minimaal acht self-hosted n8n installaties verkeerd hebben zien staan. In hun n8n docker-compose waren de workers gestart met een pruning-blok dat op papier redelijk leek:

EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=24
EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000
QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD=10000
QUEUE_RECOVERY_INTERVAL=60

De pruning-instellingen zijn op zichzelf niet het probleem. Het probleem is wat er gebeurt wanneer een job is enqueued, geen enkele worker hem oppakt binnen het BullMQ stalled-job interval, en het retry-beleid van de queue hem doorzet naar de failed set. Hun retry-config stond op drie pogingen met exponential backoff. Na drie mislukte pickup-pogingen gingen jobs naar de failed queue. Die failed queue had een TTL van één uur en een max-count van tweeduizend. Alles voorbij één van die twee plafonds werd door de BullMQ janitor opgeruimd.

In het weekend logden de workers 38.000 stalled-job errors. Elfduizend vierhonderd jobs werden ouder dan de TTL van de failed queue en werden zonder waarschuwing verwijderd. Er gaat geen belletje rinkelen als een janitor draait.

Waarschuwing

Als je retry queue een eindige TTL of een eindige max-count heeft, heb je geen retry queue. Je hebt een uitgestelde-verlies queue. Verplaats dead-lettered jobs uit Redis voordat de janitor langskomt, of accepteer dat "eventually consistent" ook "eventually weg" betekent.

Waarom niemand het zag

De klant had monitoring. De main n8n instance was gezond. De /healthz gaf 200. Webhook endpoints gaven 200. De Postgres execution-history table bleef gevuld worden, omdat een handvol cron workflows op de main instance draaide en de worker queue nooit raakte. Het Grafana-bord stond van hoek tot hoek op groen.

Wat ze niet hadden was een check op queue depth tegenover worker throughput. BullMQ stelt beide beschikbaar. Het dashboard dat ze gebruikten plotte alleen de active en waiting counts op een eenminuts gemiddelde. Waiting piekte zaterdagavond laat op 4.800 jobs, herstelde even toen de janitor de failed queue opveegde, en steeg weer. De grafiek zag eruit als een rustig zaagtandje. Voor een mens leek het op een workload-patroon, niet op een lek.

Dit is het deel dat we elke operations lead willen meegeven. Healthy is geen status flag. Healthy is een verhouding tussen wat binnenkomt en wat verwerkt wordt. Als je dashboard de vraag "heeft elke webhook het downstream effect veroorzaakt waarvoor hij bedoeld was" niet kan beantwoorden, dan is je dashboard decoratie.

De fix op maandag

Voor de lunch waren er drie wijzigingen live.

Eerst werden de workers herstart met een fatsoenlijke Sentinel client config. n8n leest Redis-instellingen uit één env-blok als je hem correct naar Sentinel wijst:

QUEUE_BULL_REDIS_HOST=
QUEUE_BULL_REDIS_PORT=
QUEUE_BULL_REDIS_SENTINEL_HOSTS=10.0.0.11:26379,10.0.0.12:26379,10.0.0.13:26379
QUEUE_BULL_REDIS_SENTINEL_NAME=mymaster
QUEUE_BULL_REDIS_SENTINEL_PASSWORD=${REDIS_SENTINEL_PASSWORD}

Let op de lege HOST en PORT. Als je die ingevuld laat, gebruikt ioredis ze stilletjes en negeert hij de sentinel-lijst. Dat is precies het valluik waar twee van hun drie workers in trapten, omdat beide env-bestanden waren gekopieerd uit het pre-Sentinel single-node tijdperk en daarna nooit zijn opgeschoond.

Ten tweede werd de failed-queue TTL verwijderd en de max-count verhoogd naar vijftigduizend, met een aparte cron die oudere dead-letters wegschrijft naar een Postgres-tabel n8n_dead_letters voor offline replay. Het replay-script is twintig regels Node en draait elke nacht. Redis is nu de in-flight retry-laag. Postgres is het duurzame kerkhof.

Ten derde werd er een opzettelijk simpel alert in Grafana gehangen: "inkomende webhooks in de laatste vijftien minuten" min "voltooide executions in de laatste vijftien minuten". Zodra dat verschil tien minuten lang boven de 200 staat, maakt PagerDuty iemand wakker. Het alert vraagt niet waarom. Het ziet alleen het gat.

De 11.400 terughalen

Greenhouse en BambooHR houden allebei aan hun kant een webhook delivery log bij met een venster van dertig dagen. We schreven een replay-job die hun delivery-log APIs uitlas, elk event met een timestamp tussen vrijdag en zondag binnenhaalde, elke payload hashte tegen wat er al in Postgres stond, en de ontbrekende terug voerde aan het n8n inbound endpoint. Zo kwamen er ongeveer 10.800 events terug.

Visma houdt geen delivery log bij. Ongeveer 600 events uit Visma payroll waren onherstelbaar. De klant belde op dinsdag de getroffen werkgevers en loste elk geval handmatig op. Het kostte de engineering lead twee dagen. Het kostte het support-team een week. Geen enkele klant vertrok, wat ons verbaasde. De les uit dat stuk: publiceer binnen vierentwintig uur een echte post-mortem, benoem wat je kapot maakte, benoem wat je veranderde, en de meeste B2B-klanten respecteren je daarna meer dan ervoor.

De architecturale les

Als jouw business afhangt van webhook-gedreven automation, moeten drie dingen waar zijn.

Je queue moet een infrastructure failover overleven. Test het. Kill de Redis master op een woensdagmiddag in staging en kijk hoe de workers opnieuw verbinden. Als ze dat niet doen, hebben je workers geen Sentinel client config. Ze hebben een fragiele pointer in een Sentinel-kostuum.

Je dead-letter queue moet ergens duurzaam draaien. Redis is prima voor in-flight retries. Het is niet de plek waar je een job de zaterdagnacht wil laten doorbrengen. Postgres, S3, alles waar geen janitor op een timer staat.

Je monitoring moet het gat meten. Niet "draait de service", maar "heeft elk event dat binnenkwam ook het werk veroorzaakt dat erbij hoort". Voor inkomende webhooks is dat telbaar. Voor complexere flows is het een verhouding tussen source-of-truth records en expected-outcome records. Bouw die diff en zet er een alert op.

Niets hiervan is nieuw. De Redis Sentinel documentatie waarschuwt onomwonden dat "the client library has to be Sentinel-aware". De n8n queue-mode docs benoemen de env-variabele val. De BullMQ docs leggen de levenscyclus van de failed queue uitgebreid uit. Het probleem is dat middelgrote teams die self-hosted automation draaien de stack meestal erven van één engineer die vertrokken is, en de volgende engineer ervan uitgaat dat de vorige de voetnoten gelezen heeft.

Wat we klanten blijven vertellen

Toen we voor deze klant de queue-laag opnieuw bouwden, verbaasde ons vooral hoe goedkoop de duurzame dead-letter was: één Postgres-tabel, één replay-script, twaalf uur werk. De andere twintig uur ging erin zitten om drie verschillende teams ervan te overtuigen dat "healthy is groen" de leugen was die hun het weekend had gekost. Als je een vergelijkbare vorm van procesautomatisering op schaal draait, is ons advies hetzelfde als aan hen: bouw eerst het gap-alert, ga daarna de rest fixen.

De vijfminutenaudit voor je eigen stack: open je queue dashboard, schrijf op hoeveel jobs er het afgelopen uur het systeem binnenkwamen, schrijf op hoeveel executions er voltooid zijn. Als het tweede getal kleiner is en je kunt niet uitleggen waar het verschil heen ging, heb je hetzelfde probleem dat zij hadden op vrijdag om 19:42.

Kern

Als je retry queue een eindige TTL of een eindige max-count heeft, heb je geen retry queue. Je hebt een uitgestelde-verlies queue.

FAQ

Treedt deze storingsmodus alleen bij n8n op?

Nee. Hetzelfde patroon raakt elke queue-laag met Redis en een eindige TTL op failed jobs. BullMQ, Sidekiq Pro, Celery op Redis, RQ. De trigger is altijd dezelfde: workers stoppen met ophalen, retries raken op, janitor veegt schoon.

Hoe test ik Sentinel failover veilig?

In staging: kill de huidige master met redis-cli SHUTDOWN NOSAVE en kijk hoe de worker-logs binnen seconden opnieuw verbinden. Herhaal het wekelijks tot het niet meer eng voelt. Production failover mag nooit de eerste keer zijn dat je dit pad ziet.

Moeten we van self-hosted n8n af en naar een hosted runner?

Niet per se. Self-hosted is prima als je de queue en de dead-letter laag behandelt als een database, niet als een cache. De meeste teams behandelen het als een cache. Dat kost ze het weekend, niet de keuze voor n8n.

Hoe groot moet een team zijn om n8n op schaal te draaien?

Eén competente platform engineer en een werkende monitoring-gewoonte. We hebben ops-teams van drie mensen het goed zien draaien en teams van twintig het slecht zien draaien. Grootte correleert nergens mee; ownership correleert met alles.

process automationautomationworkflowintegrationsarchitectureoperations

Iets bouwen?

Start een project