← Blog

Automation

Onboarding orchestration: n8n, Make, and Workers compared

Three orchestrators, one 3,200-flow weekly load, one HR-services SME in Apeldoorn. We tracked per-run cost, AVG logging, and who picks up at 04:00.

Jacob Molkenboer· Founder · A Brand New Company· 24 Sept 2025· 9 min
Three brass relay switches in a row linked by waxed thread on ivory paper, green tag on middle relay, red ribbon at edge.

It is 04:11 on a Tuesday in Apeldoorn. PagerDuty fires. An n8n self-hosted container has eaten 8 GB of RAM, the OOM killer has done its job, and 312 onboarding flows are stuck behind a dead worker. The first sync into Workday runs at 06:00. Someone has to be in front of a laptop in the next ninety minutes, or 312 new hires walk into their first day without a laptop, a building pass, or a payroll record.

That call decided the stack. The HR-services SME we built it for runs roughly 3,200 nieuwe-medewerker flows a week across forty-odd client accounts. Each flow touches between six and eleven systems: AFAS, Nmbrs, Microsoft 365, the client's Workday tenant, a CV-screening model, a background check vendor, a contract-signing tool, and a hardware ordering portal. We piloted three orchestration layers against the same load: n8n self-hosted, Make.com Enterprise, and a custom Cloudflare Workers + Hono stack. Here is what fell out.

The shape of the workload

Before any tool comparison is honest, you have to write the workload down. Ours looked like this:

  • 3,200 onboarding starts per week, with a clear Monday peak (640 to 720 starts between 08:00 and 11:00 CET).
  • Each start fans out into 7 to 14 child jobs over 72 hours, so the actual job count is closer to 32,000 per week.
  • About 4% of CVs are filtered by a model before a human reviewer sees them. Those rejections must produce an audit trail an AVG officer can read.
  • Three downstream APIs (AFAS, the Workday tenant, the background check vendor) have hard rate limits and 30 to 90 second tail latencies.
  • The team that has to maintain this is two people. Neither is a full-time platform engineer.

That last bullet matters more than the others combined. A stack that needs a dedicated SRE on call is wrong for a 24-person company, no matter how good its per-run cost looks on a spreadsheet.

Per-run cost on the same load

We ran each option for two weeks against a mirrored slice of production traffic (about 400 starts per week, fanning out to roughly 4,000 jobs). The numbers below are what we measured, not list price.

Make.com Enterprise. Make bills on operations: roughly one operation per module call. Our average flow consumed 47 operations end-to-end, with the CV-screening branch and the AFAS retry loop being the biggest spenders. After we collapsed iterators we got it down to about 12 operations per job. At 32,000 jobs per week that put us in the high-tier Enterprise band. Effective cost landed near €0.011 per run, plus €1,700/month in committed seats. The pricing is openly documented on the Make pricing page, but you only learn the real number after you instrument your own operations counter.

n8n self-hosted. Self-hosted n8n on a single Hetzner CPX31 (€16/month) plus a managed Postgres (€25/month) is almost free at small scale. At our load the worker had to be split, so we ran three workers behind a queue-mode setup. The infrastructure was about €110/month all in. Per-run cost, naively, was around €0.0009. That number is a lie until you add the on-call hours: across the two-week pilot we spent eleven engineer-hours on n8n itself (worker restarts, version bumps, one Postgres connection pool tuning). At €120/hour fully loaded, that is another €0.0041 per run. Real cost: €0.005. The queue mode docs are clear, but they assume you have someone who reads them.

Cloudflare Workers + Hono. A Worker invocation on the Paid plan costs $0.30 per million requests after the included tier, and Durable Objects add a small per-request charge for the stateful flow coordinator. We wrote one Hono-based Worker per integration and a single Durable Object class per onboarding flow. At our load the Cloudflare bill was €38/month. The R2 bucket we used for CV blobs and audit logs added another €4. Engineering time, after the first month, was effectively zero. Per-run cost: about €0.0003.

The cost story flips depending on what you count. If you count only the invoice, Workers wins by an order of magnitude. If you count the engineer hours the platform demands, Make wins for the first three months and Workers wins after.

AVG-defensible logging when a model rejects a CV

Article 22 of the GDPR (AVG in Dutch) gives a candidate the right to an explanation when an automated system materially affects them. A model that filters a CV before a human sees it is exactly that. You can read the article text on gdpr-info.eu. The short version: you need to log the input, the model version, the score, the threshold, the decision, and the human reviewer (if any) for every rejection, and keep it long enough to defend the decision but no longer than you need to.

That requirement is where the three stacks diverge most sharply.

Make stores execution logs for 30 to 90 days depending on plan, and the log format is theirs, not yours. To get an AVG-defensible trail you have to mirror every rejection event into your own store. We did that with a Make HTTP module posting to a Workers endpoint that wrote to R2. That works, but you now have two systems of record and a sync-correctness problem.

n8n keeps execution data in your Postgres. You own it, you can query it, you can prove it. The gotcha is retention. After eight months our executions table was 184 GB and growing. We tried to delete old rows by candidate ID and learned what most teams learn the hard way: at scale, in Postgres, you cannot really delete. A DELETE against a 200 GB table writes roughly its own size in WAL, bloats the heap with dead tuples that VACUUM then has to chase, and competes with the queue workers for row locks. The Postgres documentation on table partitioning spells out the alternative: partition the executions table by month, drop the old partition with DROP TABLE, and the deletion problem becomes a metadata change. Align the AVG retention window to the partition boundary and the headache disappears.

Workers + R2 sidesteps the problem differently. Each rejection event becomes an object with a key like avg-log/2026/06/16/<candidate-uuid>.json. R2 lifecycle rules expire the prefix after 24 months. There is no table to vacuum and no log format you do not control.

Warning

If your orchestrator stores your AVG-relevant logs in its own database, your retention policy is its retention policy. Mirror the rejection trail into storage you control on day one, not on the day the candidate's lawyer writes to you.

The 04:00 question

Every comparison of orchestration tools eventually arrives at the same place: at 04:00 on a Sunday, when something is broken, who picks up the phone, and what do they read?

With Make Enterprise, the answer is partly Make. Their status page covers their platform, and their support responds inside the SLA window. But the failure mode that actually happens most often (a third-party API rate-limiting, a malformed payload, a credential rotation gone wrong) is not their problem. Your runbook still has to exist.

With n8n self-hosted, the answer is you. The worker is your worker. The Postgres is your Postgres. When the worker OOMs at 04:11, no vendor cares. We learned to set a hard memory limit on the worker container, a restart policy of unless-stopped, and a separate process that drains the queue if the worker dies twice in five minutes. That works. It also has to be written, tested, and owned by someone whose name is on a calendar.

With Workers, the runtime is Cloudflare's problem. We have never been paged about a Worker invocation that did not run. We have been paged about our own code throwing, about a Durable Object hitting a storage limit we did not anticipate, and about an upstream API returning HTML instead of JSON. The runbook gets shorter because the substrate is not in it.

Where each stack actually wins

None of these tools is bad. They win different fights.

Make Enterprise wins when the team maintaining the flows is not technical, the integrations are off-the-shelf, and per-run cost is not the binding constraint. For a marketing-ops team automating Salesforce-to-HubSpot syncs at low volume, Make is the right answer and we will tell you so.

n8n self-hosted wins when you have one engineer who likes infrastructure, the flows are complex enough to need branching and custom nodes, and you want the data on your own boxes. It is a good middle ground for a 50 to 200-person company with one solid platform person.

Cloudflare Workers + Hono wins when per-run cost matters, the team writing the code is comfortable in TypeScript, and the orchestration is more code than picture. For 32,000 jobs per week with hard latency and AVG requirements, it is the answer that does not keep us up at night. The Hono framework on top of Cloudflare Workers makes the developer experience genuinely pleasant.

What we shipped

For this client we shipped the Workers stack, with one concession: the CV-screening model itself runs on a separate container behind a queue, because cold-starting a 7B model inside a Worker is still not a good idea in 2026. The orchestration, the audit trail, the retry logic, and the fan-out all live in Workers. Make is still in the diagram, but only as the surface the two non-technical operations leads use to build and edit client-specific exception flows. They get the picture-builder; we get the runtime.

One concrete pattern from the build: every onboarding flow is a single Durable Object instance keyed by candidate UUID. State transitions are explicit. The whole coordinator fits in about 900 lines of TypeScript.

import { Hono } from 'hono'
import { DurableObject } from 'cloudflare:workers'

type FlowState =
  | 'cv_received'
  | 'cv_screened'
  | 'cv_rejected'
  | 'contract_sent'
  | 'contract_signed'
  | 'systems_provisioned'
  | 'done'

interface Env {
  FLOWS: DurableObjectNamespace
  AUDIT: R2Bucket
}

function transition(state: FlowState, event: { type: string }): FlowState {
  if (state === 'cv_received' && event.type === 'screened_pass') return 'cv_screened'
  if (state === 'cv_received' && event.type === 'screened_fail') return 'cv_rejected'
  if (state === 'cv_screened' && event.type === 'contract_sent') return 'contract_sent'
  if (state === 'contract_sent' && event.type === 'contract_signed') return 'contract_signed'
  if (state === 'contract_signed' && event.type === 'provisioned') return 'systems_provisioned'
  if (state === 'systems_provisioned' && event.type === 'done') return 'done'
  return state
}

export class OnboardingFlow extends DurableObject<Env> {
  async advance(event: { type: string; payload: unknown }) {
    const state = (await this.ctx.storage.get<FlowState>('state')) ?? 'cv_received'
    const next = transition(state, event)
    await this.ctx.storage.put('state', next)
    await this.ctx.storage.put(`event:${Date.now()}`, event)
    if (next === 'cv_rejected') await this.writeAvgLog(event)
    return next
  }

  private async writeAvgLog(event: unknown) {
    const day = new Date().toISOString().slice(0, 10)
    const key = `avg-log/${day}/${this.ctx.id.toString()}.json`
    await this.env.AUDIT.put(key, JSON.stringify(event))
  }
}

const app = new Hono<{ Bindings: Env }>()
app.post('/flow/:id/event', async (c) => {
  const id = c.env.FLOWS.idFromName(c.req.param('id'))
  const stub = c.env.FLOWS.get(id) as unknown as { advance: (e: unknown) => Promise<FlowState> }
  return c.json(await stub.advance(await c.req.json()))
})
export default app

When we built the orchestration layer for this Apeldoorn HR firm, the thing we kept running into was the AVG audit trail, not the cost. We ended up putting every rejection event into R2 with a date-prefixed key, and the legal review took ninety minutes instead of three weeks. That kind of result is what most of our AI agent engagements look like in practice.

If you are about to pick an orchestrator this quarter: write the workload down first, in numbers. Then look at the per-run cost line, the AVG line, and the 04:00 line. The tool that wins on all three for your actual numbers is the one to ship. Anything else is a preference dressed up as an opinion.

Key takeaway

Pick your orchestrator on real per-run cost, AVG-defensible logging, and who owns the 04:00 page, not on a pricing-page demo.

FAQ

What does Make.com Enterprise actually cost at 3,200 weekly flows?

After collapsing iterators we consumed about 12 operations per job. That put us in the high-tier Enterprise band at roughly €0.011 per run, plus around €1,700/month in committed seats.

Can n8n self-hosted handle 32,000 jobs per week?

Yes, in queue mode with three workers behind a Postgres-backed queue. Plan for memory limits, restart policies, and a partitioning strategy on the executions table so retention deletes stay tractable.

Does Cloudflare Workers work for stateful onboarding flows?

Yes, via Durable Objects. One instance per candidate UUID makes state transitions explicit and keeps the runtime concern off your 04:00 runbook.

How do you keep CV-rejection logging AVG-defensible?

Log input, model version, score, threshold, decision, and reviewer for every rejection. Store it in a system you control, and align retention to a partition boundary so deletes stay possible at scale.

Why not just stay on Make for everything?

Make is excellent for non-technical teams and off-the-shelf integrations, but at 32,000 jobs per week with AVG requirements the per-run cost and log-ownership picture stops working.

automationprocess automationworkflowarchitectureai agentscase study

Building something?

Start a project