Build a production-safe n8n error handling workflow by generating an idempotency key early, writing it to a dedupe store with a unique constraint, using node-level retry settings and routing exhausted attempts to a dead-letter sub-workflow so failures do not cause duplicate sends or silent crashes.
Why this keeps happening
n8n workflow failures commonly happen when external APIs time out, a node is retried blindly, or parallel branches race to perform the same side effect. If the workflow has no idempotency check, retries or invisible crashes lead to duplicate emails, duplicate writes or lost alerts. Forum threads show users seeing retries re-send emails, or multiple parallel sub-workflows crashing without recorded errors, which is why a defensive pattern is needed.
Step by step fix
-
Create a stable idempotency key. Use a Code node to build a deterministic key from the incoming webhook or payload, for example customer id plus event type plus the provider id. Example Code node output:
const p = items[0].json; const key = `${p.customerId || p.id}_${p.event || p.type}`; return [{ json: { idempotencyKey: key, payload: p } }]; -
Write to a dedupe store with a uniqueness constraint. Use a lightweight Postgres table or Redis with a SETNX style lock. For Postgres, create a table with unique index on
idempotency_key. Example SQL to run once in your DB:
Then use the Postgres node to run this parameterized insert, with conflict handling:CREATE TABLE dedupe ( id SERIAL PRIMARY KEY, idempotency_key TEXT UNIQUE, payload JSONB, created_at TIMESTAMP DEFAULT now() );
If a row is returned the run is first time, otherwise treat it as duplicate and stop further side-effect nodes.INSERT INTO dedupe (idempotency_key, payload) VALUES ($1, $2) ON CONFLICT (idempotency_key) DO NOTHING RETURNING id; -
Set node-level retry handling. Configure node retry attempts and delay in the node settings panel, and avoid global blind retries. When a node calls an external API, set a modest retry count and a backoff delay. If your plan or version lacks per-node retry UI, wrap the call in a small loop in a Code node or use a Wait node between attempts. Do not mark intermittent network errors as success with the
Continue On Failtoggle without recording the failure. -
Move exhaustion to a dead-letter workflow. Add an
Execute Workflownode to call a dedicated DLQ workflow when attempts exceed threshold. Pass the original payload,idempotencyKey, and error text. The DLQ workflow can notify a Slack channel, create a ticket, and store the failed payload for manual replay. - Log and alert. Record attempt counts and error messages in a monitoring log table or external tool. Send alerts only from the DLQ path so you are not flooded by transient failures. Include the idempotency key in every log line for fast lookup.
n8n error handling workflow: practical example
Below is a concise n8n pattern you can copy into your workflow. Nodes, in order: Webhook trigger, Code (build key), Postgres (dedupe insert), IF (check insert returned id), HTTP Request (external send), Execute Workflow (dead-letter). Use the Postgres node to detect duplicates and stop the flow early.
Code node snippet to build key (already shown) should produce idempotencyKey. Postgres insert uses that key as the $1 parameter and the payload as $2. Then use an IF node that checks whether Postgres returned a row. If true continue to the HTTP Request node, if false end the run with a short log message.
When the HTTP Request node returns an error, the node-level retry settings should fire. After all retries fail, connect the error path to the Execute Workflow node to push the run to your DLQ workflow where you can inspect and reprocess safely without risk of duplicates.
Tool-specific notes
- Postgres vs Redis, Postgres is simplest if you already use SQL. Use a unique index and
ON CONFLICT DO NOTHING. Redis with SETNX is lighter, but you will need a Redis client or Webdis bridge since n8n lacks a built-in Redis set node in some versions. - Make.com, create a deterministic webhook id and write it to a global datastore module or a database with unique constraint. Use the router and flow control to stop duplicate paths. Scenarios allow iterator and error handlers you can reuse similarly.
- Zapier, store a unique key in Storage by Zapier or your database and bail out when the key exists. Zapier has built-in deduplication for many triggers but persistent store is required for cross-Zap idempotency.
How to stop it happening again
Adopt idempotent external calls, add a DB-level uniqueness check so duplicates cannot be written even if workflow logic fails, and centralize retry policy. Avoid relying on opaque cloud retries alone. Test failure modes by simulating API timeouts and by forcing duplicate webhook deliveries. See real reports of retries re-sending emails on the forum so you know this is not hypothetical, for example this thread about retries re-sending messages and this general discussion of how others handle n8n workflow failures.
When to get help
If you see silent crashes with no node error logs, or retries marked as success incorrectly, open a support ticket with n8n Cloud, and capture a failing execution JSON for debugging. If you want hands-on help to implement this pattern in your stack, read our Automate Naija playbook on retrying from failed nodes and preventing duplicates, or contact a freelancer who can add Postgres/Redis dedupe and a DLQ workflow for you.
Put this pattern into a template: Code node for idempotency key, Postgres dedupe insert, IF node to gate side effects, node retry settings, and Execute Workflow for dead-lettering. That combination covers n8n retry handling and prevents duplicate runs while giving you a safe path for manual fixes.
Learn to build automations like this with Automate Naija.
Related reading: forum report of retries re-sending emails, and community discussion on handling workflow failures. Also see our guide on retrying from the failed node.
Frequently asked questions
How do I stop n8n from sending duplicate emails after a retry?
Generate a deterministic idempotency key at the start of the workflow, write that key to a dedupe store with a unique index, and gate the email send behind a successful insert. If the insert finds a duplicate, skip the send. Also use node-level retries and route exhausted attempts to a dead-letter workflow for inspection.
Should I use Postgres or Redis for deduplication?
Use Postgres if you already have it, because a unique constraint and ON CONFLICT DO NOTHING is simple and reliable. Use Redis if you need very low latency and have an easy way to call Redis from n8n, for example via a Redis bridge or custom node. Both work if you implement a strict uniqueness check.
What node settings control retries in n8n?
Look in the node settings panel for retry or retry on fail options in your n8n version. If those are not available, implement retries with a loop in a Code node combined with a Wait node, or use a controlled wrapper that attempts the HTTP Request a fixed number of times before failing and sending to the DLQ.
When should I route a failed run to a dead-letter workflow?
Route to a dead-letter workflow after your configured number of retries is exhausted, or after detecting an unrecoverable error like authentication failure. The DLQ should record the payload, idempotency key and error, notify the team, and allow safe manual replay without risk of duplicates.
Build it with us. The community is free and the build track teaches this kind of automation week by week: see pricing or join the community.