One of my own workflows — the gate that decides whether a new WhatsApp contact gets the bot or gets me — failed 24 times between Thursday and Sunday. Seventy-one hours. I found out on Monday morning, by opening the executions list for an unrelated reason.
That is embarrassing for someone who sells automation. So instead of fixing the one workflow and moving on, I went and checked error-handling coverage across my production instance. It was far lower than the number in my head.
n8n 2.35.5, queue mode, Postgres.
Why silence is the default
Error handling in n8n is opt-in, per workflow. You build one workflow whose trigger is the Error Trigger node, then in every other workflow you open Settings → Error Workflow and point it at that one. There is no switch that applies it to everything you have already built, or to everything you build next.
That is a per-workflow checkbox on a list that grows every week. Nobody forgets on purpose. You just build yet another workflow at 11pm, it works, you activate it, and you move on. The failure mode of an opt-in safety net is that coverage decays quietly while the number in your head stays at "yeah, we have error handling."
Mine had decayed further than I would have guessed.
What the gap actually cost
I pulled the failures of my own internal workflows, grouped by workflow, with a column for whether that workflow had an error handler attached:
| Workflow | Guarded | Failures | Window |
|---|---|---|---|
| WhatsApp group lead detection (AI) | no | 34 | 36.5 h |
| Personal-line bot gate | no | 24 | 70.9 h |
| Campaign-failure watchdog | no | 11 | 8.7 h |
| Website lead intake form | no | 4 | 53.0 h |
Read the last column, not the failure counts. The longest is 70.9 hours — a WhatsApp bot gate that failed 24 times over three days while I was working on other things. Nothing was on fire. No customer wrote in. The bot simply did not answer new contacts, and the only reason I found it is that I went looking.
The workflows that did have an error handler attached failed in the same week too, and none of them stayed broken for long, because something told me.
That is the whole value proposition of the Error Trigger, and it is not "fewer failures." It is failures that end in hours instead of days.
The part that actually scared me
A few of my workflows contain an Error Trigger node. Those are the handlers — the things that send the Telegram alert. Every one of them was itself unguarded, and the one covering the most workflows failed during that same week, leaving everything it guarded with no alerting at all and no way to find out. The error handler cannot report its own errors, because the thing that reports errors is the error handler. Quis custodiet ipsos custodes, in YAML.
You cannot fix this by pointing handler A at handler B, either. That just moves the single point of silence one hop and adds a cycle you will forget about. The catcher has to be watched from outside n8n.
The pattern, in four parts
1. One handler, not one per project. Fan-in beats fan-out. A single Error Trigger workflow that formats {{ $json.workflow.name }}, {{ $json.execution.id }} and {{ $json.execution.error.message }} into one alert is easier to keep correct than six near-copies.
2. Audit coverage with a query, not with your memory. This is the whole audit — run it against your n8n Postgres:
SELECT w.name,
(w.settings->>'errorWorkflow' IS NOT NULL
AND w.settings->>'errorWorkflow' <> '') AS guarded
FROM workflow_entity w
WHERE w.active = true
ORDER BY guarded, w.name;
Everything with guarded = f is a workflow that can fail into the void. If you are on n8n Cloud, the same check runs off GET /api/v1/workflows?active=true and reading settings.errorWorkflow per item.
That query is now the first line of my weekly runbook, next to the backup check. A new f row gets treated the way a failing test does.
3. Make the handler loud about workflows, not just about errors. The first thing I changed in mine was putting the workflow name in the subject line instead of in the body. That sounds trivial. It is the difference between "an automation failed" (which you snooze) and "the lead intake failed" (which you do not).
4. Guard the guard from outside. The Error Trigger has a blind spot that no amount of coverage fixes: it fires when an execution fails. It does not fire when an execution never starts — a deactivated trigger, a dead cron, a webhook whose URL changed, a worker that is not consuming the queue. A workflow that stopped running entirely produces zero failed executions and therefore zero alerts. It looks exactly like a quiet week.
So the handler pings a dead man's switch on every run, and a plain cron on a different machine screams if the ping stops:
# in the error handler workflow, and in one heartbeat workflow per instance
curl -fsS -m 10 "https://your-monitor/ping/$SLUG" || true
# elsewhere — not on the n8n box
LAST=$(stat -f %m /var/lib/heartbeat/n8n1 2>/dev/null || echo 0)
if [ $(( $(date +%s) - LAST )) -gt 3600 ]; then
notify "n8n1 heartbeat is $(( ($(date +%s) - LAST) / 60 ))m stale"
fi
The || true matters: a monitoring call that can fail the run it is monitoring is worse than no monitoring.
That is it. Four parts, none clever. The reason it is worth writing down is that I had parts 1 and 3 for as long as I have run this instance and still ate a three-day outage, because I never had part 2 and never imagined I needed part 4.
Where I landed
I am not going to pretend this is finished. As I write this, coverage is still well short of complete. What changed is that I now have the query that produces the number and a list of unguarded workflows in the order I am going to fix them, longest silent window first. The handler-watching-the-handler problem is the part I am fixing today, because it is one workflow and a lot depends on it.
The honest reason I am writing this before finishing it: the audit took twenty minutes and the three-day outage took three days. Those twenty minutes are the cheapest thing in the entire stack I maintain — cheaper than the WhatsApp bots I run in production for clients, and a rounding error against the numbers in this breakdown of what business automation actually costs. Run the query before you finish reading this post and you will probably learn something uncomfortable too.
One genuine question, because I do not think I have solved this part: how do you monitor the trigger that never fires? A heartbeat proves the instance is alive and proves that workflow ran. It does not prove that a webhook from a third party still arrives, or that a specific cron survived the last upgrade. Per-workflow "expected minimum run rate" alerting is the obvious answer and it also sounds like a maintenance burden that decays exactly like error-workflow coverage did.
If you run n8n in production: do you alert on absence of runs, and if so — how do you keep the thresholds from rotting?
Top comments (3)
This landed the morning after I ran the same audit on my own catalog — uncomfortable timing, in a good way.
I'm on the template-seller side of this (verified n8n creator, 5 templates through official review). My "production" is 21 JSON files that strangers import into their instances. Two nights ago I grep'd the whole catalog for retry/error handling: zero. 13 HTTP nodes across 8 files — one failed API call kills the workflow silently. Same disease, different host.
What I shipped:
retryOnFailon every HTTP node (3 retries, 5s backoff), re-imported all 8 files into a real n8n instance (8/8 clean), rebuilt the download ZIPs, and wrote the whole thing up transparently — including what I didn't fix: dev.to/aiautomationplaybook/i-ran-...Your part 4 is the one I can't fix from a JSON file, and I said so in the post: retry handles "the call failed," it does nothing for the trigger that never fires. The most a template seller can honestly ship is (1) retry on every HTTP node, (2) a setup guide documenting the Error Trigger + external heartbeat pattern, (3) never calling anything "production-ready" that hasn't been imported and run. The monitoring has to live with the buyer.
On your open question — from the seller side, per-workflow "expected run rate" alerting rots exactly like coverage does, so I wouldn't prescribe thresholds. What I'd prescribe is your part 2: the audit query on a schedule, new
frows treated as failing tests. That's the only part that doesn't depend on anyone remembering."Failures that end in hours instead of days" — stealing that line.
On your open question: I give each critical workflow a "last healthy run" timestamp and alert when it goes stale against its expected cadence. The cheap version is a second workflow that reads the execution list via the API and pings you when the newest successful run is older than X. It won't catch everything, but it catches the dead cron.
For workflows with a predictable cadence, I’d alert on a stale last-success timestamp against an explicit expectation, then audit that every active workflow has an owner and a current threshold. For irregular webhooks, a heartbeat only proves the monitor path is alive; it can’t prove the third-party event still arrives. A periodic synthetic event (where it’s safe) or an upstream delivery log gives that separate signal. I’d keep these checks outside the workflow they monitor, and review the thresholds as part of the same scheduled coverage audit so they don’t quietly rot.