Make n8n workflows reliable: errors, retries and alerts

Most n8n workflows work fine on the day you build them. The trouble starts later. An API times out, a field arrives empty, or a webhook fires twice, and nobody notices until a customer asks where their invoice went.

author
Zahara Schaefer
published
updated
read
7 min
tested
n8n 2.x self-hosted (node and setting names from the n8n docs; UI labels may shift between minor releases)
outline {
  1. What you need
  2. 1. Add retries to nodes that call other systems
  3. 2. Decide what happens when a node fails
  4. 3. Build one error workflow for everything
  5. 4. Split big workflows into sub-workflows
  6. 5. Make webhooks safe to receive twice
  7. 6. Limit how much runs at once
  8. 7. Keep an eye on it
  9. Troubleshooting
  10. References

This guide covers the handful of n8n features that turn a working workflow into one you can leave alone:

  • retries;
  • error outputs;
  • a central error workflow;
  • sub-workflows;
  • protection against duplicates;
  • concurrency limits.

It assumes you already have n8n running. If not, start with Self-host n8n with Docker Compose.

What you need

  • n8n 2.x, self-hosted or n8n Cloud.
  • A workflow that matters to you, to practise on.
  • Somewhere to send alerts: email (SMTP), Slack, Discord, Telegram, or anything n8n has a node for.

Step 1: Add retries to nodes that call other systems

Every node has a Settings tab. On any node that calls an external API (HTTP Request, a CRM node, an email node), open it and turn on:

  • Retry On Fail;
  • Max Tries: for example 3;
  • Wait Between Tries (ms): for example 2000.

That handles short outages and rate limits without any extra nodes. Don't add retries to nodes that aren't safe to repeat, such as "create invoice" on an API that doesn't support idempotency keys, unless you've done step 5.

Check it worked: point an HTTP Request node at https://httpbin.org/status/503 and run it. The execution log shows the node tried three times before failing.

Step 2: Decide what happens when a node fails

In the same Settings tab, On Error has three options:

OptionWhat happensUse it when
Stop Workflow (default)The execution fails at this nodeThe rest of the workflow makes no sense without this step
ContinueThe workflow carries on using the last valid dataRarely. It hides failures.
Continue (using error output)The node gets a second output, and failed items go down itYou want to handle the failure: log it, alert someone, or take another path

Continue (using error output) is the one to learn. For example, a "Create contact in CRM" node can send its failures to a Google Sheets or Postgres node that records the item and the error message, while successful items carry on.

Check it worked: with an error output connected, run the workflow with bad input. The failed item appears on the error branch, and the execution shows as succeeded.

Step 3: Build one error workflow for everything

When an execution fails outright, n8n can start a separate workflow. Build it once and use it everywhere.

  1. Create a new workflow called Error handler.
  2. Add an Error Trigger node as the first node.
  3. Add your alert node, for example Send Email or Slack. Build a message from the trigger's data:
text
Workflow failed: {{ $json.workflow.name }}
Node: {{ $json.execution.lastNodeExecuted }}
Error: {{ $json.execution.error.message }}
Execution: {{ $json.execution.url }}
  1. Save it. An error workflow doesn't need to be activated.
  2. In each production workflow, open Workflow settings (the menu at top right) and set Error workflow to Error handler.

The execution URL only works if n8n knows its public address, which is the N8N_EDITOR_BASE_URL or webhook URL setting covered in the self-hosting guide.

To fail a workflow on purpose, for example when a required field is missing, use the Stop and Error node. It ends the execution with your message, and that triggers the error workflow.

Check it worked: add a Stop and Error node to a test workflow, link it to Error handler, run it in production (activate it, then trigger it). The alert arrives. Manual test runs from the editor don't call the error workflow.

Step 4: Split big workflows into sub-workflows

A 60-node workflow is hard to read and hard to test. Pull repeated or self-contained parts out:

  1. Create a new workflow that starts with When Executed by Another Workflow (the Execute Workflow Trigger). Define the input fields it expects.
  2. In the parent workflow, add an Execute Workflow node (shown as "Execute Sub-workflow" in recent versions), and pick the sub-workflow from the list.
  3. Choose whether the parent waits for the result (the default) or carries on.

Good candidates: "find or create contact", "format address", and "send notification". When the CRM changes, you fix one sub-workflow instead of twelve copies.

Check it worked: the parent's execution log links to the sub-workflow execution, and the sub-workflow's output appears as the node's output.

Step 5: Make webhooks safe to receive twice

Senders retry. Stripe, GitHub and most form tools resend a webhook if they don't get a fast success response, so your workflow will sometimes see the same event twice.

Two habits help:

  • Respond quickly. Set the Webhook node's Respond option to Immediately, or use a Respond to Webhook node early in the workflow. Then the sender doesn't time out and retry while slow steps run.
  • Check for duplicates. Most senders include an event ID. Before doing anything with side effects, look it up:
  1. Create a table, for example in Postgres:
sql
CREATE TABLE processed_events (
  event_id text PRIMARY KEY,
  status text NOT NULL CHECK (status IN ('received', 'done')),
  received_at timestamptz NOT NULL DEFAULT now()
);
  1. After the Webhook node, add a Postgres node. Put this in Query, and put {{ $json.body.id }} in Query Parameters (so the value is $1, not concatenated into the SQL):
sql
INSERT INTO processed_events (event_id, status)
VALUES ($1, 'received')
ON CONFLICT (event_id) DO UPDATE
  SET event_id = EXCLUDED.event_id
WHERE processed_events.status <> 'done'
RETURNING event_id;
  1. Add an If node: if the Postgres node returned a row, continue. If it returned nothing, stop: that event is already done.
  1. After the last side-effect node, add another Postgres node (same Query Parameters) that marks it finished:
sql
UPDATE processed_events SET status = 'done' WHERE event_id = $1;

If a later node fails, leave status as received (or delete the row on an error branch). A retry can then claim the event again. Recording the ID as finished *before* the side effects is what drops retried webhooks.

n8n also has a Remove Duplicates node that can remember values across executions. It's fine for small volumes. A database table is easier to inspect and clean up.

Check it worked: send a payload, fail a node after the insert, then send the same payload again. The retry should run the side effects. A third send, after a successful run, should stop at the If node.

Step 6: Limit how much runs at once

By default, a single n8n instance runs production executions without a limit. A burst of webhooks can then flood a slow API or use up the server's memory. Set a limit with an environment variable:

yaml
environment:
  N8N_CONCURRENCY_PRODUCTION_LIMIT: 10

Executions above the limit wait in a queue and run in order. The number is yours to choose. Start low, and raise it while watching CPU, memory and your API rate limits. In queue mode the same variable, if set to a value other than -1, overrides each worker's --concurrency (default 10). See Control concurrency.

For APIs with strict rate limits, also use Loop Over Items with a Wait node inside the loop, or the batching option on the HTTP Request node.

Check it worked: fire 20 webhooks at once:

bash
for i in $(seq 1 20); do curl -s -o /dev/null -X POST https://n8n.example.com/webhook/hello -d "{\"n\":$i}" -H 'Content-Type: application/json' & done; wait

The Executions list shows some of them as queued before they run.

Step 7: Keep an eye on it

  • Executions list: filter by Error once a week, even with alerts on.
  • Metrics: set N8N_METRICS=true to expose a Prometheus /metrics endpoint, if you run Prometheus.
  • Pruning: EXECUTIONS_DATA_PRUNE=true and EXECUTIONS_DATA_MAX_AGE (in hours) stop execution history from filling the database.
  • Version control: export workflows regularly with n8n export:workflow --all --separate --output=<folder> and commit them, so you can see what changed.

Troubleshooting

  • The error workflow never fires: it only runs for production executions (active workflows triggered for real), and only if the failing workflow has it set under Workflow settings > Error workflow.
  • Retries make things worse: the node isn't safe to repeat. Add an idempotency key or the duplicate check from step 5 first.
  • `$json.body.id` is empty: the sender puts the ID somewhere else. Look at a real execution's input data and adjust the expression.
  • Queued executions never start: check the server's memory and the logs (docker compose logs n8n). A stuck long-running execution can hold a slot.

References