Tooling
n8n productie-cheatsheet: nodes die je beter vermijdt
De HTTP Request node retried stilletjes. De Code node heeft geen tests. De AI Agent node verbrandt budget. Dit vervangen we, en dit houden we.

De factuur-chase workflow van een klant draaide vrijdag schoon. Zondagochtend gingen er 200 aanmaningen uit naar klanten die al betaald hadden. De HTTP Request node tikte hun facturatiesysteem aan, kreeg een 200 terug met een lege body, n8n las dat als een lege array, en het filter "staat in de betaalde lijst?" gaf voor iedereen nee terug. De finance-afdeling was maandag bezig met excuusmails.
Die workflow stond al zes maanden in productie. De bug zat er al die tijd in. Hij kwam pas naar boven toen de billing-API rate-limited raakte en lege 200's begon terug te geven in plaats van 429's.
n8n is een nuttig stuk gereedschap. Wij gebruiken het ook. De zichtbare delen van een pipeline (de triggers, simpele data-bewegingen, de dingen die een operations-lead in een diagram wil zien) horen er thuis. De broze delen niet. Dit is de cheatsheet die we klanten meegeven bij een audit van een bestaande instance.
De vier nodes die we als concept behandelen
HTTP Request
Default timeout is 300 seconden. Retries staan default uit en zijn stil als ze aanstaan. Er is geen veld voor een idempotency-key. Geen circuit breaker. De response parser maakt vrolijk een lege array van een lege 200, zoals hierboven. Heeft de upstream API een eigenaardigheid (en die hebben ze allemaal), dan beschermt de HTTP Request node je daar niet tegen.
De node is prima voor een enkele call naar een tamme interne API die je zelf in de hand hebt. Het is geen infrastructuur.
Code (JavaScript)
De Code node draait nu in isolated-vm. Vroeger draaide hij in vm2, dat in 2023 werd afgeschreven na een serie sandbox-escapes (CVE-2023-29017 was de luidste). De sandbox-vraag is daarmee gesloten. De operationele vraag niet.
Code in een Code node heeft geen versiebeheer buiten de workflow-JSON, geen tests, geen PR-review, geen linter, geen types. De diff die een bug introduceert is onzichtbaar tot iemand de workflow exporteert en 4.000 regels JSON gaat lezen. Wij hebben die oefening gedaan. Niet leuk.
AI Agent en LangChain chain nodes
Tokenverbruik is onbegrensd. Er zit geen budgetcap per run in de node. Prompts staan in de workflow-JSON, dus prompt-wijzigingen laten geen audit-trail achter. JSON-parsing op model-output faalt stilletjes. De "memory" sub-nodes houden conversation-state in de n8n-database, waar die database niet voor gebouwd is.
Voor een prototype waarin je één keer een model aanroept, prima. Voor een agent die duizend keer per dag op klant-e-mail draait, niet.
Wait en Schedule Trigger combinaties
Een Wait node bewaart workflow-state in de n8n execution-tabel. Een workflow die zeven dagen wacht op een "heeft de klant gereageerd"-check houdt zeven dagen lang een open execution-rij. Herstart je de n8n-container, dan ben je de in-memory delen kwijt. De Schedule Trigger gaat niet schoon om met de zomertijd op uurlijkse crons. Wij hebben workflows twee keer zien vuren bij de overgang in maart.
Heeft jouw n8n-instance op een willekeurig moment meer dan 50 actieve executions in de "waiting"-status, dan gebruik je n8n als durable execution engine. Dat is het niet.
De vier vervangers die we ernaast zetten
Geen van deze is exotisch. Allemaal open source, allemaal draaien op een kleine VPS, allemaal staan ze naast de n8n-instance en worden vanuit n8n aangeroepen.
Hono service voor HTTP-grenzen
Iedere externe HTTP-call die ertoe doet krijgt een dunne Hono-wrapper. De wrapper bezit de timeout, het retry-beleid, de idempotency-key en de response sanity checks. n8n roept één endpoint per integratie aan, niet de ruwe upstream.
import { Hono } from 'hono'
const app = new Hono()
app.post('/billing/check-paid', async (c) => {
const { invoiceId } = await c.req.json()
const res = await fetch(`${BILLING_URL}/invoices/${invoiceId}`, {
headers: { 'X-Idempotency-Key': invoiceId },
signal: AbortSignal.timeout(8_000),
})
if (!res.ok) throw new Error(`billing ${res.status}`)
const text = await res.text()
if (!text) throw new Error('billing returned empty body')
const data = JSON.parse(text)
return c.json({ paid: data.status === 'paid' })
})
export default app
De zondagochtend-aanmaningsbug van hierboven gebeurt hier niet. Een lege body gooit een error. Een error wordt in n8n een fout, en daar hangt een zichtbare Error Workflow aan. De operations-lead krijgt een Slack-bericht om 9 uur in plaats van 200 excuusmails om 11 uur.
BullMQ voor fan-out en retries
BullMQ op Redis doet alles wat moet kunnen retrien, dedupen of parallel draaien. n8n triggert de job door een HTTP-endpoint aan te tikken. Het endpoint zet hem in de queue. De worker doet het werk.
import { Queue } from 'bullmq'
const reminders = new Queue('reminders', { connection: redis })
await reminders.add(
'send',
{ invoiceId, to: customer.email },
{
attempts: 5,
backoff: { type: 'exponential', delay: 1_000 },
jobId: `reminder:${invoiceId}`, // dedup, dezelfde factuur kan niet twee keer in de queue
},
)
De jobId-regel is de regel die ertoe doet. Het is ook de regel die een Code node niet zou hebben.
Inngest voor durable workflows
Het "wacht zeven dagen en check dan"-patroon gaat naar Inngest. De open source build is Apache 2.0 en draait op één Postgres plus één Go-binary. Wij zetten 'm naast n8n neer.
import { Inngest } from 'inngest'
const inngest = new Inngest({ id: 'invoice-chase' })
export const chase = inngest.createFunction(
{ id: 'chase-overdue', retries: 4 },
{ event: 'invoice/overdue' },
async ({ event, step }) => {
const paid = await step.run('check-paid', () =>
fetch(`${API}/billing/check-paid`, {
method: 'POST',
body: JSON.stringify({ invoiceId: event.data.id }),
}).then((r) => r.json()),
)
if (paid.paid) return { skipped: true }
await step.sleep('wait-3d', '3d')
const stillUnpaid = await step.run('recheck', () => checkAgain(event.data.id))
if (stillUnpaid) await step.run('send-reminder', () => sendEmail(event.data))
},
)
Elke step.run is een checkpoint. Herstart je de worker, dan pakt de functie op vanaf de laatst voltooide stap. De wachttijd van zeven dagen zit niet in de execution-tabel van n8n, maar in de state store van Inngest, waar die store voor bedoeld is.
Een kleine function service voor de rest
Voor het werk dat vroeger in een Code node leefde, houden we één repo met TypeScript-functies. Elke functie is één HTTP-endpoint. Elke functie heeft tests. Elke functie gaat via CI. De workflow roept ze aan zoals elke andere HTTP-integratie.
Klinkt zwaarder dan het is. De eerste functie kost een middag. De vijfde kost tien minuten. De winst: niemand hoeft meer workflow-JSON te exporteren om een logische wijziging te reviewen.
Wat in n8n blijft
De triggers blijven. Het flow-diagram blijft. De als-dit-dan-dat branching blijft. De "kijk naar de laatste zeven runs en zie wat er gebeurde"-UI blijft. n8n is goed in de zichtbare laag zijn.
Het patroon is hetzelfde als bij elke low-code tool. Gebruik 'm voor de delen die baat hebben bij zichtbaarheid. Zet de delen die correct, snel of auditable moeten zijn erachter.
Toen we de factuur-chase AI-agents herbouwden voor een Nederlandse accountancyklant, slonk de workflow-JSON van 87 nodes naar 11. De 11 die overbleven zijn de nodes waar de finance-lead ook echt naar wil kijken. De rest leeft in een TypeScript-repo van 600 regels met tests, achter een Hono-service, in de queue via BullMQ.
Audit van vijf minuten die je vandaag kunt doen: open je n8n-instance, filter de Executions-view op status "error" voor de laatste 30 dagen, en tel hoeveel fouten in HTTP Request, Code of AI Agent nodes leven. Dat aantal is je prioriteitenlijst voor vervanging.
Kern
n8n is het dashboard, niet de motor. Zet de HTTP-, code-, agent- en lange-wacht-nodes achter een kleine service die jij beheert.
FAQ
Is n8n een verkeerde keuze voor productie?
Nee. Het is een goede keuze voor de zichtbare orchestratielaag. Het probleem ontstaat wanneer de broze delen (HTTP-calls, model-calls, lange wachttijden, ongeteste code) ín de workflow leven in plaats van erachter.
Waarom Inngest in plaats van Temporal?
Allebei werken. Inngest heeft een lichtere footprint en een snellere onboarding voor teams die al TypeScript schrijven. Temporal wint bij polyglotte teams of als je sterkere garanties nodig hebt rond exactly-once-semantiek.
Gebruiken jullie de n8n Code node nog wel ergens voor?
Ja, voor eenregelige transformaties (een veld hernoemen, een status-string mappen). Alles langer dan tien regels of dat een test nodig heeft, gaat naar de function service repo.
En de AI Agent node voor simpele prompts?
Prima voor een prototype. Zodra de prompt invloed heeft op een klant of op volume geld kost, gaat hij naar een service waar prompts versie-beheerd zijn, kosten gecapt zijn en outputs gevalideerd worden.