Automation
Workflow-orchestratie bij 240k/dag: n8n, Windmill, Temporal
Een Utrechtse SaaS-club vroeg ons de orchestratie-backbone te kiezen voor 240.000 webhook events per dag. We benchmarkten n8n, Windmill en Temporal op dezelfde hardware.

De briefing kwam binnen op een dinsdag. Een SaaS-bedrijf in Utrecht met 34 mensen, net na hun Series A, verkoopt een logistieke integratietool. Hun pipeline vangt ongeveer 240.000 webhook events per dag op van vervoerders, marketplaces en klant-ERP's. De twee engineers in het platformteam liepen vast op Cloudflare Workers en queue-lijmcode, en vroegen ons de orchestratie-backbone te kiezen waar ze de komende drie jaar mee gaan werken.
Ze hadden een shortlist: n8n self-hosted, Windmill, Temporal. We benchmarkten alle drie op dezelfde hardware, met dezelfde realistische load, en draaiden daarna een chaos-test die je op geen enkele marketingpagina tegenkomt. Dit zijn de cijfers die eruit kwamen.
De workload
240.000 inkomende events per dag. Pieken rond 9 events per seconde aanhoudend, bursts tot 80 per seconde als een grote vervoerder om 02:00 UTC een batch releases stuurt. Elk event triggert een keten van 4 tot 9 stappen: signature valideren, verrijken vanuit interne Postgres, dedupliceren tegen Redis, schrijven naar de warehouse, fan-out naar één of drie downstream API's. Latency-budget end-to-end: 800ms p95, anders gaat de vervoerder retry'en en verwerken we dubbel.
Hardware voor de bench: één Hetzner AX52 (Ryzen 7700, 64GB RAM, NVMe). Postgres 16 op dezelfde bak. Zelfde netwerk, zelfde load generator (een vegeta-script dat 24 uur geanonimiseerd productieverkeer replayt).
n8n self-hosted
n8n is het voor de hand liggende startpunt. Node.js, visuele editor, honderden community-nodes. We deployden het in queue mode met twee worker-containers, Postgres als execution store, Redis BullMQ als broker. De documentatie is prima als je 'm twee keer leest.
Het pluspunt: een ops-persoon zonder engineering-achtergrond kan de workflow lezen. We hadden een finance lead die zelf een retry-branch aanpaste, en dat soort dingen telt zwaarder dan throughput in een bedrijf van 34 mensen.
Het minpunt dook op rond 60 events per seconde aanhoudend. De queue accepteerde vrolijk werk. De executie liep daarna vol. Workers waren CPU-bound op signature-validatie, die n8n draait in hetzelfde Node-proces als de workflow engine. p95 step latency liep op van 110ms in rust naar 480ms onder burst.
Cold start van één workflow met drie stappen, gemeten vanaf POST /webhook tot de eerste node draait: tussen 80 en 220ms, afhankelijk van of de execution data op warme pages stond. Niet traag. Ook niet gratis, vermenigvuldigd met 9 miljoen invocaties per maand.
Windmill
Windmill is de nieuwere kandidaat. Rust core, TypeScript en Python workers, gebouwd door een ex-Palantir-team in Parijs. Het voelt alsof iemand de editor van n8n heeft gepakt en de runtime opnieuw heeft gebouwd, deze keer eerlijk over cold starts.
We draaiden Windmill met zes worker-processen tegen dezelfde Postgres-instantie. De signature-validatiestap schreven we in Deno; de enrichment-stap in een Python-script dat httpx importeerde. Beide hergebruikten warme worker pools.
Cold start, dezelfde workflow met drie stappen: 14 tot 40ms. De Rust-scheduler laadt geen JavaScript-interpreter om te beslissen wat er draait. Dat klinkt voor de hand liggend, maar het verklaart het verschil. Bij 80 per seconde burst bleef de bak onder de 35% CPU en hield p95 latency op 190ms.
Twee wrijvingspunten die we tegenkwamen, geen van beide gedocumenteerd. Ten eerste: de gedeelde Python worker pool is geweldig, totdat twee scripts verschillende httpx-versies pinnen. We moesten pools splitsen per script-familie. Windmill ondersteunt dat wel, maar de UI laat het niet duidelijk zien. Ten tweede: workflow-versionering is per flow, niet per step. Pas je een step aan waar 12 flows naar verwijzen, dan krijg je 12 nieuwe flow-versies en wordt het audit log een rommeltje. Het Windmill-team reageert vlot op hun GitHub issue tracker, en dat telt zwaar als je een platform durft te zetten op een tool die nog op een eencijferige versie staat.
Temporal
Temporal is het zwaargewicht. Het is eigenlijk geen concurrent van de andere twee; het is een andere soort tool. Je schrijft workflows als code, deterministisch, en de Temporal-server logt elk event zodat een worker mid-step kan crashen en verder kan vanaf precies de instructie waarop hij stierf.
We draaiden Temporal met een Postgres persistence backend, officieel ondersteund sinds 1.20. Eén worker pool in Go, één in TypeScript.
Cold start is de verkeerde vraag voor Temporal. Workers blijven warm en pollen. Wat telt is de worker-startuptijd bij een deploy: activities en workflows registreren duurde 1,8 tot 3,2 seconden in onze setup. Dat is het venster waarin een rolling deploy events kan laten vallen als je niet goed draint.
Throughput was prima. Postgres werd de bottleneck rond 140 events per seconde, ruim boven de piek van de klant. De production checklist in hun docs is opvallend eerlijk over wat er stuk gaat op schaal.
De prijs zit in het developer-model. Een workflow-functie mag geen Date.now() of Math.random() aanroepen, en geen directe netwerk-I/O doen; alles loopt via activities. Het platformteam had dit in een week onder de knie. Niemand buiten het platformteam komt eraan. Dat is prima als je dat vooraf accepteert.
Cold starts die de docs overslaan
Cold start is geen enkel getal. Meet time-to-first-step, time-to-first-log-line en time-to-completion. De drie lopen onder load flink uit elkaar.
Dit is het meetscript dat we gebruikten. Pas het aan naar de tool van jouw keuze.
#!/usr/bin/env bash
# Measure orchestrator cold start under realistic burst
# Usage: ./bench.sh https://orchestrator.local/webhook/test
URL="$1"
TMP=$(mktemp)
for i in $(seq 1 500); do
start=$(date +%s%3N)
curl -sS -o /dev/null \
-H "Content-Type: application/json" \
-d '{"id":"'$i'","payload":"x"}' "$URL"
end=$(date +%s%3N)
echo "$((end - start))" >> "$TMP"
done
sort -n "$TMP" | awk '
{ a[NR]=$1 }
END {
print "p50:", a[int(NR*0.5)]"ms"
print "p95:", a[int(NR*0.95)]"ms"
print "p99:", a[int(NR*0.99)]"ms"
}'
Wat we zagen over 500 opeenvolgende cold invocations op de bench-bak:
- n8n: p50 92ms, p95 210ms, p99 380ms
- Windmill: p50 18ms, p95 41ms, p99 78ms
- Temporal: p50 24ms, p95 55ms, p99 95ms (workers warm)
Dit zijn onze cijfers op onze hardware. Die van jou worden anders. De vorm niet.
Replay, het stuk dat om 3 uur 's nachts toebijt
Cold start is een marketingcijfer. Replay is een outagecijfer.
We simuleerden een worker crash midden in een flow op elk platform, en stelden daarna de vraag die er echt toe doet: kan de volgende worker oppakken waar de dode bleef, zonder side-effects opnieuw uit te voeren die al gebeurd zijn?
- n8n slaat execution data per node op. Replay herstart vanaf de laatst voltooide node. Heeft je node een side-effect dat al is uitgevoerd (een Stripe-charge, een Slack-post), dan vuurt het opnieuw. Je kunt afdekken met idempotency keys in de aangeroepen services, en dat moet je doen.
- Windmill gebruikt een vergelijkbaar model, met optionele per-step retries en een resume-from-step knop voor admins. Zelfde caveat als bij n8n. De UI laat beter zien wat al gedraaid heeft.
- Temporal doet deterministische replay; dat is het hele punt. De worker reconstrueert state vanuit de event-history en slaat al voltooide activities over. De Stripe-charge vuurt niet opnieuw, omdat Temporal weet dat hij al teruggaf. Dit is het ene ding dat de andere twee per design niet kunnen evenaren.
Raken je workflows geld, voorraad of iets met juridisch gewicht verderop, dan telt dit verschil zwaarder dan welk cold-startcijfer dan ook. Zijn je workflows fan-out enrichment die veilig idempotent is, dan telt het minder.
Idempotency keys in downstream API's zijn geen vervanging voor replay safety. Ze zijn een vangnet. We hebben dubbele charges van vier cijfers gezien bij teams die het andersom dachten.
Wat we kozen
Voor het Utrechtse team leverden we Windmill op. De keuze viel op drie dingen: cold-start ruimte voor 5x groei, het platformteam kon de runtime-code lezen (Rust plus TypeScript, niet Go plus history-event), en één van de ops-mensen kon flows blijven bouwen zonder de discipline van workflow-as-code te leren.
Temporal hielden we achter de hand voor de billing pipeline, die ze in Q3 migreren. Twee engines, één orchestratiegrens, geen excuses nodig.
Het agent-werk waar de HN-frontpage deze week over praat (agent runtimes, Codex-driven engineering) komt hier ook terecht. Webhook-orchestratie vandaag is agent-orchestratie morgen. Dezelfde primitieven, langer levende state, dezelfde behoefte aan een runtime die een gefaalde step kan replayen zonder dat je het substraat over 18 maanden opnieuw moet bouwen.
Het kleinste wat je vandaag kunt doen
Draai het bench-script hierboven tegen je huidige orchestrator met 500 opeenvolgende invocaties op een rustig moment. Is p99 meer dan 5x je p50, dan betaal je elke dag cold-start belasting die je niet meet. Dat is genoeg informatie om te beslissen of de rest van deze post een vrijdagmiddag waard is. Toen we de process automation-laag voor deze klant bouwden, was throughput niet het probleem. Het probleem was dat de visuele editor waar niet-engineers van hielden bij n8n het replay-model voor ze verborg, en ze het pas merkten toen een Stripe-webhook tijdens een onderhoudsvenster dubbel afging. We hebben het opgelost door geld-rakende flows via Temporal te routeren en de rest op Windmill te houden.
Kern
Cold start is een marketingcijfer. Replay is een outagecijfer. Kies de orchestrator wiens replay-model past bij je ergste flow.
FAQ
Moet ik voor n8n kiezen als mijn team niet vol engineers zit?
Ja, als je event-volume onder de ongeveer 30 per seconde aanhoudend blijft en je flows geen geld raken. De visuele editor is de echte kracht en bespaart weken werk.
Kan Temporal n8n of Windmill vervangen?
Niet voor niet-engineers. Temporal-workflows zijn code met strikte determinismeregels. Het vult de andere twee aan voor high-value flows; het vervangt ze niet.
Is Windmill productieklaar voor serieuze workloads?
Ja, voor orchestratie tot een paar honderd events per seconde op standaard hardware. Let op gedeelde Python worker dependency-conflicten en gaten in per-step versionering in de UI.
Wat is de grootste cold-start valkuil?
Meet p99 onder burst, niet p50 in rust. Het verschil tussen die twee is de belasting die je betaalt tijdens je drukste uur, en de meeste vendor-docs noemen alleen het vriendelijke getal.