Bekzod Erkinov
Listen to Article
Loading...Table of contents · 9 sections
n8n vs Zapier: Self-Host n8n in Docker and Find the Break-Even
You have a webhook that needs a bit of JavaScript and an outbound HTTP call, and you're deciding whether to pay for a hosted tool or run n8n yourself. By the end you'll have a working n8n container with that workflow imported, a backup you have restored, RAM and latency numbers from my run (raw output included), and tables showing at what monthly volume self-hosting saves money on paper in 2026.
Some links in this post are affiliate links; see our disclosure.
I ran n8n. I did not run Zapier or Make: for those two I read the pricing pages and help docs on 2026-10-10 and compared documented limits and prices. Nothing below claims how they perform.
Run n8n in Docker
The n8n Docker docs use the n8nio/n8n image, port 5678, and a volume at /home/node/.n8n. On 2026-10-10 the page lists 2.42.6 as stable and carries a banner saying the content is outdated in favor of Docker Compose (see our Docker Compose walkthrough for a Laravel app for the Compose syntax). Plain docker run still worked for me, and docker run --rm n8nio/n8n --version printed 2.42.6.
The workflow's HTTP node calls http://echo:9000/hook, so n8n and a tiny echo server share a Docker network. The commands are POSIX shell (I used Git Bash on Windows).
docker network create n8nnet
docker run -d --name echo --network n8nnet node:22-alpine node -e "require('http').createServer((q,r)=>{let b='';q.on('data',d=>b+=d);q.on('end',()=>{r.setHeader('content-type','application/json');r.end(JSON.stringify({received:JSON.parse(b)}))})}).listen(9000)"
docker volume create n8n_bench
docker run -d --name n8nb --network n8nnet -p 5678:5678 \
-e GENERIC_TIMEZONE=UTC -e TZ=UTC \
-e N8N_DIAGNOSTICS_ENABLED=false \
-v n8n_bench:/home/node/.n8n \
n8nio/n8n
Set both timezone variables: GENERIC_TIMEZONE controls schedule nodes, TZ the container clock.
Import the webhook, transform, HTTP workflow
The workflow is a Webhook node (POST, path bench, respond with the last node's output), a Code node that totals line items, and an HTTP Request node that posts the result to the echo server. Save it as wf.json:
[{
"id":"bench1","name":"bench webhook transform http","active":false,
"nodes":[
{"parameters":{"httpMethod":"POST","path":"bench","responseMode":"lastNode","options":{}},"id":"n1","name":"Webhook","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[0,0],"webhookId":"11111111-1111-1111-1111-111111111111"},
{"parameters":{"jsCode":"const b = $input.first().json.body;\nconst total = b.items.reduce((s,i)=>s+i.qty*i.price,0);\nreturn [{json:{order:b.order.toUpperCase(),total:Math.round(total*100)/100,count:b.items.length}}];"},"id":"n2","name":"Transform","type":"n8n-nodes-base.code","typeVersion":2,"position":[220,0]},
{"parameters":{"method":"POST","url":"http://echo:9000/hook","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify($json) }}","options":{}},"id":"n3","name":"HTTP Request","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[440,0]}
],
"connections":{"Webhook":{"main":[[{"node":"Transform","type":"main","index":0}]]},"Transform":{"main":[[{"node":"HTTP Request","type":"main","index":0}]]}},
"settings":{"executionOrder":"v1"}
}]
Import and publish it from the CLI, call the webhook, restart, call it again:
docker cp wf.json n8nb:/tmp/wf.json
docker exec n8nb n8n import:workflow --input=/tmp/wf.json
docker exec n8nb n8n publish:workflow --id=bench1
curl -s -XPOST localhost:5678/webhook/bench -H 'content-type: application/json' \
-d '{"order":"ab-1","items":[{"qty":2,"price":9.99},{"qty":1,"price":5}]}'
docker restart n8nb
Real output from my run:
Importing 1 workflows...
Successfully imported 1 workflow.
Publishing workflow with ID: bench1 (current version)
Note: Changes will not take effect if n8n is running.
Please restart n8n for changes to take effect if n8n is currently running.
--- before restart:
{"code":404,"message":"The requested webhook \"POST bench\" is not registered.","hint":"The workflow must be active for a production URL to run successfully. ..."} [HTTP 404]
Publishing against a running n8n left the production webhook unregistered until a restart. After a restart the webhook also took a while to come up, so don't assume it is live the moment the container starts (the restore test below shows the same). Once it was up:
$ curl -s -XPOST localhost:5678/webhook/bench -H 'content-type: application/json' \
-d '{"order":"ab-1","items":[{"qty":2,"price":9.99},{"qty":1,"price":5}]}'
{"received":{"order":"AB-1","total":24.98,"count":2}}
Use /webhook/bench, not /webhook-test/bench; the test URL only listens while you click "Listen for test event" in the editor. For more on handling webhooks safely, see our Stripe webhooks and order lifecycle post.
What it cost in RAM and time
Machine: a Windows 11 laptop, Docker Desktop, 8 CPUs and about 7.7 GiB visible to Docker. One machine, a few runs, not a benchmark.
RAM. I sampled docker stats --no-stream in a loop during an earlier run. Raw lines (first and last sample of each phase, middle cut):
stats idle 384.3MiB / 7.755GiB cpu=0.15%
stats sequential/load 395.3MiB / 7.755GiB cpu=89.28%
...
stats sequential/load 458.4MiB / 7.755GiB cpu=37.23%
stats 5-parallel/load 481.6MiB / 7.755GiB cpu=99.37%
...
stats 5-parallel/load 543.2MiB / 7.755GiB cpu=28.89%
stats 20-parallel/load 548.8MiB / 7.755GiB cpu=121.37%
...
stats 20-parallel/load 533.1MiB / 7.755GiB cpu=0.48%
stats after+30s 472.5MiB / 7.755GiB cpu=0.35%
Idle was 384 MiB, and RAM did not return to idle 30 seconds after load. I don't know whether that is cache, uncollected garbage or a leak.
Latency. That earlier run also measured latency, but the sampler was hitting the Docker API in a tight loop at the same time, so its numbers (sequential p50 306 ms, 2.9 req/s) are upper bounds. I re-ran latency with no sampler, against the container restored from the backup below. Save as bench.py:
import time, json, statistics, urllib.request, concurrent.futures as cf
body = json.dumps({"order": "ab-1", "items": [{"qty": 2, "price": 9.99}, {"qty": 1, "price": 5}]}).encode()
ERR = []
def hit():
t = time.perf_counter()
r = urllib.request.Request("http://localhost:5678/webhook/bench", body, {"content-type": "application/json"})
try: urllib.request.urlopen(r, timeout=30).read()
except Exception as e: ERR.append(type(e).__name__); return None
return (time.perf_counter() - t) * 1000
def run(tag, workers, n):
ERR.clear()
t = time.perf_counter()
with cf.ThreadPoolExecutor(workers) as ex: res = list(ex.map(lambda _: hit(), range(n)))
d = time.perf_counter() - t
ok = sorted(x for x in res if x)
print("%s: workers=%d n=%d ok=%d failed=%d total=%.1fs %.1f req/s p50=%.0fms p95=%.0fms errors=%s" % (
tag, workers, n, len(ok), n - len(ok), d, len(ok) / d, statistics.median(ok), ok[int(len(ok) * .95) - 1], sorted(set(ERR))), flush=True)
for _ in range(10): hit()
run("sequential", 1, 200)
run("5-parallel", 5, 200)
run("20-parallel", 20, 400)
Raw output, plus one docker stats reading taken afterwards:
sequential: workers=1 n=200 ok=199 failed=1 total=60.7s 3.3 req/s p50=146ms p95=297ms errors=['TimeoutError']
5-parallel: workers=5 n=200 ok=200 failed=0 total=36.2s 5.5 req/s p50=916ms p95=1334ms errors=[]
20-parallel: workers=20 n=400 ok=400 failed=0 total=57.7s 6.9 req/s p50=2708ms p95=4347ms errors=[]
623.9MiB / 7.755GiB cpu=3.40%
One sequential request hit my 30-second client timeout and I did not find out why; it is also why the sequential total is 60.7 s. Throughput stayed around 3 to 7 requests per second regardless of client count, while latency grew with the number of clients.
Limits of this test
- One laptop with 8 CPUs. I never ran on a 1 vCPU or 1 to 2 GiB server, so the server size priced below is an assumption, not a tested floor.
- The RAM figures come from the earlier sampled run and the latency figures from the unsampled re-run; they are separate runs.
- I did not test a version upgrade with database migrations, only the backup and restore below.
- The echo server is local, so a real endpoint adds its own latency.
Backup and restore
Backup (n8n stopped so the SQLite file isn't mid-write), then restore into a fresh volume and start a new container from it:
docker stop n8nb
docker run --rm -v n8n_bench:/data -v "$(pwd -W):/backup" alpine tar czf /backup/n8n_bench.tgz -C /data .
docker start n8nb
docker volume create n8n_restored
docker run --rm -v n8n_restored:/data -v "$(pwd -W):/backup" alpine tar xzf /backup/n8n_bench.tgz -C /data
docker run -d --name n8nr --network n8nnet -p 5678:5678 \
-e GENERIC_TIMEZONE=UTC -e TZ=UTC -e N8N_DIAGNOSTICS_ENABLED=false \
-v n8n_restored:/home/node/.n8n n8nio/n8n:latest
$(pwd -W) is the Git Bash form; use $PWD on Linux. The archive is 1,505,234 bytes (1.5 MB) and the tar xzf restore step took 2.7 s. I then polled the webhook every 3 seconds (HTTP status codes; 000 means connection refused):
1 000
2 000
3 000
4 000
5 000
6 404
7 404
8 200
{"received":{"order":"AB-1","total":24.98,"count":2}}
The restored volume brought the workflow back and it answered correctly, roughly 20 to 25 seconds after docker run. I checked only the workflow definition, not credentials or execution history.
Price the same workload on Zapier and Make
This workflow is a trigger plus two actions. How each vendor counts that matters more than the headline price:
- Zapier: triggers never use tasks; successful action steps do. Code by Zapier uses 1 task at standard runtime. The page does not mention the Webhooks action, so I assume it counts as one task, making this flow 2 tasks per run (1 if only Code is used). The Free plan is 100 tasks and doesn't include webhooks.
- Make: each module action is one credit, and the Make Code app costs 2 credits per second. I assume 3 credits per run (webhook, a built-in transform, HTTP). Core is $9/month for 10,000 credits, about 3,300 runs on that assumption. When I fetched the pricing page on 2026-10-10, its slider listed more Core tiers (20k, 40k, 80k, 150k credits and up) but showed a price only for 10k, so I cannot give a Make-versus-server crossover above 10k credits. Free is 1,000 credits and 2 active scenarios.
- n8n Cloud, per the n8n pricing page as fetched on 2026-10-10: Starter €20/month for 2.5K executions and Pro €50/month for 10K, both billed annually; Business (self-hosted) €667/month billed annually for 40K executions. Monthly-contract prices are not shown.
Self-hosted cost: a DigitalOcean 2 GiB / 1 vCPU droplet is $12/month (1 GiB is $6). n8n Community Edition itself costs nothing.
Break-even by monthly volume: n8n vs Zapier cost
Zapier prices are Professional-plan tiers from the pricing page, annual billing (monthly billing is higher). "Saving" is Zapier annual minus the $12 server. "Upkeep to erase it" is how much of your own maintenance time, at an assumed $50/hour (use your own rate), cancels the saving. That column is arithmetic, not a measurement.
2 tasks per run (webhook action plus Code):
| Runs / month | Tasks | Zapier Pro | Saving vs $12 server | Upkeep to erase it |
|---|---|---|---|---|
| 500 | 1,000 | $39 | $27 | 32 min |
| 2,500 | 5,000 | $89 | $77 | 1.5 h |
| 10,000 | 20,000 | $189 | $177 | 3.5 h |
| 25,000 | 50,000 | $289 | $277 | 5.5 h |
1 task per run (webhook trigger plus only Code by Zapier):
| Runs / month | Tasks | Zapier Pro | Saving vs $12 server | Upkeep to erase it |
|---|---|---|---|---|
| 500 | 500 | $19.99 | $7.99 | 10 min |
| 2,500 | 2,500 | $89 | $77 | 1.5 h |
| 10,000 | 10,000 | $129 | $117 | 2.3 h |
| 25,000 | 25,000 | $289 | $277 | 5.5 h |
Make Core at $9 is cheaper than the $12 server up to roughly 3,300 runs a month on the 3-credit assumption, with nothing for you to maintain.
The license is part of the cost
n8n isn't OSI open source. Its Sustainable Use License lets you use it "only for your own internal business purposes or for non-commercial or personal use." Running your own automations, even at a company, is fine. Offering n8n workflows to your customers as a hosted product is not; the n8n pricing page lists the €667/month Business tier above for the self-hosted case.
Decision rule
One recommendation per cell, on the assumptions above (3 Make credits per run, 2 Zapier tasks per run, $12 server):
| Monthly runs | A developer maintains it | A non-developer maintains it |
|---|---|---|
| Under ~50, no webhook | Zapier Free or Make Free | Zapier Free or Make Free |
| Up to ~3,300 | Make Core, $9 | Make Core, $9 |
| ~3,300 to 10,000 | Look up your Make tier price; if it beats $12 plus your upkeep, stay on Make, otherwise self-host n8n | Look up your Make tier price; use Zapier if Make lacks a module you need |
| Above 10,000 | Self-host n8n (Zapier is $189 in the 2-task table at 10,000 runs) | n8n Cloud (€50 for 10K executions) or Zapier |
Make's prices above 10k credits weren't visible, so the two middle-right cells tell you what to compare rather than a number. Extra action steps add tasks on Zapier but nothing in n8n, which favors self-hosting; upgrades, backups and "why did it stop" time favor the hosted tools.
What to do next
- Count your actual monthly runs and steps. For Zapier, subtract trigger, Filter, Paths and Formatter steps from the total.
- Run the commands above and import the workflow on your laptop first, then restore your backup once before trusting it.
- Replace the echo URL with your real endpoint, run
bench.py, and record your own RAM before choosing a server size. - Under ~3,300 runs a month, open Make and build this flow before you buy a server. If you need an integration Make lacks, try Zapier.
Keep reading
Complete Solution: Scaling a Node.js Application with Kubernetes and Docker
29 min · 896 views
Data ScienceMastering CI/CD Pipelines with Jenkins and Docker: A Deep Dive into Automated Deployment and Testing
14 min · 884 views
AI WorkflowsDecentralized Finance Protocol Comparison: Uniswap V3 vs SushiSwap vs Curve Finance - Performance, Security, and Use Cases
16 min · 775 views
Bekzod Erkinov
AuthorFounder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.
Get the AI-Assisted Developer's Field Guide
The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.
Comments (0)
Please log in to leave a comment.
Log InRelated Articles
Decentralized Finance Protocol Comparison: Uniswap V3 vs SushiSwap vs Curve Finance - Performance, Security, and Use Cases
Feb 28, 2026
PostgreSQL Hidden Features for Better Performance: Battle-Tested Optimizations from Production
May 23, 2026
Claude Code Headless: Automate Dev Chores with claude -p
Oct 6, 2026