<p><!-- HERO IMAGE --></p>
<figure class="wp-block-image aligncenter"><img src="https://flowgenius.in/wp-content/uploads/2026/01/n8n-idempotency-retry-failures.png" alt="Step by Step Guide to solve n8n idempotency retry failures" /><figcaption style="text-align: center; margin: 15px;">Step by Step Guide to solve n8n idempotency retry failures</figcaption></figure>
<hr style="margin: 55px 0;" />
<p><!-- ── STORY HOOK ──────────────────────────────────────────────── --></p>
<div style="background: #fff8f0; border-left: 4px solid #e07b39; padding: 24px 28px; margin-bottom: 2.5em; border-radius: 0 6px 6px 0;">
<p style="margin: 0 0 1em 0; line-height: 1.9; font-size: 1.05em;">I shipped a Stripe payment workflow on a Friday. By Monday morning, 11 customers had been charged twice. Support tickets were piling up. The workflow hadn’t crashed – it had done exactly what I built it to do: retry on failure.</p>
<p style="margin: 0; line-height: 1.9; font-size: 1.05em;">The real bug wasn’t in the code. It was that I’d never made the workflow <strong>idempotent</strong>. This guide exists so you don’t have that Monday.</p>
</div>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Who this is for:</strong> n8n developers and automation engineers who need reliable, production‑grade workflows that avoid duplicate side‑effects. We cover this in detail in the <a href="/n8n Production Failure Patterns Guide">n8n Production Failure Patterns Guide</a>.</p>
<hr style="margin: 55px 0;" />
<p><!-- ── DEBUG DECISION TREE ─────────────────────────────────────── --></p>
<h2 style="margin-bottom: 16px; line-height: 1.3;">🔍 Debug Decision Tree: Find Your Fix in 60 Seconds</h2>
<p style="margin-bottom: 1.5em; line-height: 1.9;">Most devs get stuck here: they know something duplicated, but have no systematic way to pinpoint <em>where</em> the guard failed. Use this tree before doing anything else.</p>
<pre style="background: #f4f4f4; padding: 28px; border: 1px solid #ddd; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 2;">Are you seeing duplicate records / emails / charges?
│
├── YES — Did the same n8n execution run more than once?
│ │
│ ├── YES (check Execution Log: same execution_id ran 2×)
│ │ └── ➜ Webhook re-delivery problem
│ │ Fix: Add idempotency gate BEFORE first side-effect node
│ │ (see Section 2: Webhook Re-delivery below)
│ │
│ └── NO (different execution_ids, same payload)
│ │
│ ├── Did a node retry after a timeout / 5xx?
│ │ └── YES ➜ Missing idempotency key on HTTP Request node
│ │ Fix: Section 3.3 — attach Idempotency-Key header
│ │
│ └── Did you manually re-run the workflow?
│ └── YES ➜ No pre-call DB guard in place
│ Fix: Section 3.2 — INSERT ON CONFLICT pattern
│
└── NO — Getting "pending" status stuck in idempotency_log?
│
├── Older than 30 min → external API never returned 2xx
│ Fix: Section 5 — Grafana query to find stale keys
│
└── Race condition (two parallel branches wrote same key simultaneously)
Fix: Section 3.5 — Circuit Breaker pattern below
</pre>
<hr style="margin: 55px 0;" />
<p><!-- ── QUICK DIAGNOSIS ─────────────────────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">Quick Diagnosis</h2>
<p style="margin-bottom: 1.2em; line-height: 1.9;"><strong>Problem:</strong> Re‑running a failed n8n workflow (or a node inside it) writes the same data twice, creates orphan records, or triggers side‑effects such as duplicate emails or payments.</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Featured‑snippet solution:</strong> Add an <strong>idempotency key</strong> to every external call, store the key with a status flag, and configure the node’s <strong>Retry</strong> options to <strong>“Skip on success.”</strong> This guarantees that a second attempt sees the previous successful execution and aborts without writing again.</p>
<hr style="margin: 55px 0;" />
<p><!-- ── SECTION 1: WHAT IS IDEMPOTENCY ─────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">1. What “Idempotency” Means in n8n?</h2>
<p style="margin-bottom: 1.5em; line-height: 1.9;">If you encounter any <a href="/n8n-partial-failure-handling">n8n partial failure handling</a> issues, resolve them before continuing with the setup.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Term</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">n8n Context</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Typical Outcome</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Idempotent operation</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">A node that can be executed multiple times with the same input and produce the <strong>exact same result</strong> (no extra side‑effects).</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Safe retries, no duplicate records.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Non‑idempotent operation</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Nodes that trigger external actions (e.g., <code>HTTP Request</code>, <code>Send Email</code>, <code>Stripe Charge</code>) that <strong>create</strong> resources on each call.</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Duplicate emails, double‑charged payments, extra DB rows.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Idempotency key</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">A deterministic identifier (UUID, hash of payload, or business key) attached to the request.</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Used by the downstream system to detect repeats.</td>
</tr>
</tbody>
</table>
<blockquote style="margin: 0 0 2em 0; padding: 16px 20px; border-left: 4px solid #e0e0e0; background: #fafafa; border-radius: 0 6px 6px 0; font-style: italic;">
<p style="margin: 0; line-height: 1.9;"><strong>Note:</strong> Many SaaS APIs (Stripe, PayPal, SendGrid) require the key in a specific header (<code>Idempotency‑Key</code>). If the API does <strong>not</strong> support it, implement a <em>local</em> guard (e.g., DB lock) to achieve the same effect.</p>
</blockquote>
<hr style="margin: 55px 0;" />
<p><!-- ── SECTION 2: COMMON SCENARIOS ────────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">2. Common Scenarios That Trigger Idempotency Failures</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 3em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Scenario</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Why it fails</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Automatic retries</strong> (default 3×) on a node that creates a record</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Each retry repeats the <code>POST</code> request → two rows in DB.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Manual re‑run</strong> of a failed workflow</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">The workflow starts from the first node again, replaying all side‑effects.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Parallel branches</strong> writing to the same resource</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Race condition: both branches write before the other sees the commit.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Webhook re‑delivery</strong> (e.g., Stripe webhook retried after 30 s)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">n8n receives the same event twice and forwards it downstream.</td>
</tr>
</tbody>
</table>
<p><!-- Webhook Flow Diagram --></p>
<hr style="margin: 55px 0;" />
<h3 style="margin-bottom: 16px; line-height: 1.3;">How Webhook Re-delivery Creates Duplicates – The Exact Flow</h3>
<p style="margin-bottom: 1.2em; line-height: 1.9;">This is where most devs get stuck: Stripe or Shopify retries the webhook because your workflow took too long to respond. n8n treats it as a brand-new execution. No built-in deduplication kicks in.</p>
<pre style="background: #f4f4f4; padding: 28px; border: 1px solid #ddd; border-radius: 6px; overflow: auto; font-size: 0.9em; line-height: 2;">Stripe n8n Webhook Your Workflow
│ │ │
│── POST /webhook ──────────>│ │
│ │── triggers execution ───>│
│ │ │── charges card ──> ✅ SUCCESS
│ │ │
│ (timeout / slow response) │ │
│<── 504 Gateway Timeout ────│ │ │ │ │ │ (Stripe retries 30s later)│ │ │── POST /webhook ──────────>│ │
│ │── NEW execution ────────>│
│ │ │── charges card ──> ❌ DUPLICATE
Fix: Respond 200 OK immediately, THEN process.
→ n8n setting: Webhook node → "Respond" → "Immediately"
→ Guard checks idempotency key BEFORE any side-effect node runs.
</pre>
<hr style="margin: 55px 0;" />
<p><!-- ── SECTION 3: BUILDING AN IDEMPOTENT WORKFLOW ─────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">3. Building an Idempotent n8n Workflow</h2>
<p><!-- 3.1 --></p>
<h3 style="margin-top: 36px; margin-bottom: 16px; line-height: 1.3;">3.1 Generate a Stable Idempotency Key</h3>
<p style="margin-bottom: 1.5em; line-height: 1.9;">Create a <strong>Set</strong> node that derives a deterministic key from business data:</p>
<pre style="background: #fafafa; padding: 24px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 1.8;">{
"name": "Set Key",
"type": "n8n-nodes-base.set",
"parameters": {
"values": [
{
"name": "idempotencyKey",
"value": "={{ $json['orderId'] + '_' + $json['customerId'] }}",
"type": "string"
}
]
}
}
</pre>
<p style="margin-top: 1.2em; margin-bottom: 2em; line-height: 1.9;"><strong>Tip:</strong> Use a business‑unique attribute (order ID, email) plus a static prefix. For pure randomness, replace the value with <code>{{ $uuid }}</code>.</p>
<p><!-- SHA256 callout --></p>
<div style="background: #f0f7ff; border-left: 4px solid #2e75b6; padding: 20px 24px; margin: 2em 0; border-radius: 0 6px 6px 0;">
<p style="margin: 0 0 0.6em 0; line-height: 1.9;"><strong>Production-grade alternative: SHA256 fingerprinting</strong></p>
<p style="margin: 0; line-height: 1.9; color: #444;">When you don’t have a stable business key (e.g. processing raw webhook payloads), generate a collision-resistant fingerprint from the payload content instead. Add a <strong>Code</strong> node immediately after your Webhook node:</p>
</div>
<pre style="background: #fafafa; padding: 24px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 1.8;">// Code node — runs before any side-effect node
const crypto = require('crypto');
const input = $input.first().json;
// Hash only the stable fields — exclude volatile ones like timestamps
const stablePayload = {
event_type: input.type,
resource_id: input.data?.object?.id,
amount: input.data?.object?.amount
};
const idempotencyKey = crypto
.createHash('sha256')
.update(JSON.stringify(stablePayload))
.digest('hex');
return [{ json: { ...input, idempotencyKey } }];
</pre>
<p style="margin-top: 1.2em; margin-bottom: 3em; line-height: 1.9; color: #555;">⚠️ <strong>Do not include volatile fields</strong> like <code>Date</code> headers, <code>request_id</code>, or random salts in the hash. The key must be identical across all retry attempts for the same logical event.</p>
<hr style="margin: 55px 0;" />
<p><!-- 3.2 --></p>
<h3 style="margin-bottom: 16px; line-height: 1.3;">3.2 Store the Key Before the External Call</h3>
<p style="margin-bottom: 1.5em; line-height: 1.9;">Choose a storage backend that matches your load:</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Storage option</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">When to use</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Considerations</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>n8n DB (SQLite)</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Small volume, low traffic</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">SQLite locks can deadlock under high concurrency — add a <code>busy_timeout</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>External DB (PostgreSQL / MySQL)</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">High‑throughput, multi‑instance n8n</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use <strong>SERIALIZABLE</strong> isolation to avoid phantom reads.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Cache (Redis)</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Fast look‑ups, TTL‑based expiry</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">TTL must exceed the longest possible retry window.</td>
</tr>
</tbody>
</table>
<p><!-- Stuck callout --></p>
<div style="background: #fff3cd; border-left: 4px solid #f0ad4e; padding: 18px 22px; margin: 2em 0; border-radius: 0 6px 6px 0;">
<p style="margin: 0; line-height: 1.9;">⚠️ <strong>Most devs get stuck here:</strong> They write the idempotency key to the DB <em>after</em> the external call succeeds. This is backwards. If the external call times out midway and n8n retries, the key was never written — so the guard misses it and the duplicate fires anyway.</p>
<p><strong>Always write the key with status <code>pending</code> BEFORE the external call.</strong></p>
</div>
<p style="margin-top: 2em; margin-bottom: 1em; line-height: 1.9;"><strong>Insert‑if‑not‑exists (PostgreSQL)</strong> — this query creates a log entry only once:</p>
<pre style="background: #fafafa; padding: 24px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 1.8;">INSERT INTO idempotency_log (key, status, created_at)
VALUES ($1, 'pending', NOW())
ON CONFLICT (key) DO UPDATE
SET status = EXCLUDED.status
RETURNING status;
</pre>
<p style="margin-top: 1.2em; margin-bottom: 3em; line-height: 1.9;">If the returned <code>status</code> is <code>completed</code>, <strong>skip</strong> the downstream call entirely.<br />
If you encounter any <a href="/n8n-long-running-workflow-failures">n8n long running workflow failures</a>, resolve them before continuing.</p>
<p><!-- Real log snippet --></p>
<h4 style="margin-bottom: 12px; line-height: 1.3;">What a duplicate-causing retry looks like in the n8n Execution Log</h4>
<p style="margin-bottom: 1em; line-height: 1.9; color: #555;">This log pattern is the tell-sign that idempotency is broken — two executions, same input, both showing <code>success</code>:</p>
<pre style="background: #1e1e1e; color: #d4d4d4; padding: 24px; border-radius: 6px; overflow: auto; font-size: 0.88em; line-height: 1.9;">[2026-01-14 02:47:11] Execution #4821 — STARTED
Trigger: Webhook POST /stripe-charge
Payload: { orderId: "ord_9kX2", customerId: "cus_Rp7", amount: 4900 }
[2026-01-14 02:47:12] Node "HTTP Request: Charge Stripe" — SUCCESS
Response: { id: "ch_3Oa1", status: "succeeded", amount: 4900 }
[2026-01-14 02:47:12] Execution #4821 — FINISHED (1.2s)
────────────────────────────────────────────────────────────────
[2026-01-14 02:47:41] Execution #4822 — STARTED ← Stripe retry after 30s timeout
Trigger: Webhook POST /stripe-charge
Payload: { orderId: "ord_9kX2", customerId: "cus_Rp7", amount: 4900 } ← SAME PAYLOAD
[2026-01-14 02:47:42] Node "HTTP Request: Charge Stripe" — SUCCESS
Response: { id: "ch_3Oa2", status: "succeeded", amount: 4900 } ← SECOND CHARGE 💥
[2026-01-14 02:47:42] Execution #4822 — FINISHED (1.1s)
</pre>
<p style="margin-top: 1.2em; margin-bottom: 3em; line-height: 1.9; color: #555;">If you see two <code>FINISHED</code> executions with identical payloads and different <code>ch_</code> IDs, your guard is either missing or placed <em>after</em> the charge node — not before it.</p>
<hr style="margin: 55px 0;" />
<p><!-- 3.3 --></p>
<h3 style="margin-bottom: 16px; line-height: 1.3;">3.3 Attach the Key to the External Call</h3>
<p style="margin-bottom: 1.5em; line-height: 1.9;">Map the key to the header expected by the target API:</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">API</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Header name</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">n8n node field</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Stripe</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>Idempotency-Key</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Headers → Idempotency-Key</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">SendGrid</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>X-Message-Id</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Headers → X-Message-Id</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Custom REST</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>Idempotency-Key</code> (or custom)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Headers → Idempotency-Key</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 1em; line-height: 1.9;">Add the header inside the <strong>HTTP Request</strong> node configuration:</p>
<pre style="background: #fafafa; padding: 24px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 1.8;">{
"name": "Create Order",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://api.example.com/orders",
"method": "POST",
"jsonParameters": true,
"options": {
"bodyContentType": "json",
"bodyParametersJson": "={{ $json }}",
"headerParametersJson": "{\"Idempotency-Key\":\"{{ $json.idempotencyKey }}\"}"
}
}
}
</pre>
<hr style="margin: 55px 0;" />
<p><!-- 3.4 --></p>
<h3 style="margin-bottom: 16px; line-height: 1.3;">3.4 Mark Success / Failure After the Call</h3>
<p style="margin-bottom: 1.5em; line-height: 1.9;">After the request returns, use a <strong>Set</strong> node to update the log record:</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 3em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Outcome</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>2xx</strong> (success)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Update <code>idempotency_log.status = 'completed'</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>4xx / 5xx</strong> (error)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Keep <code>status = 'pending'</code> so the next retry can attempt it again.</td>
</tr>
</tbody>
</table>
<hr style="margin: 55px 0;" />
<p><!-- 3.5 --></p>
<h3 style="margin-bottom: 16px; line-height: 1.3;">3.5 Circuit Breaker — Stop the Cascade Before It Starts</h3>
<p style="margin-bottom: 1.2em; line-height: 1.9;">Idempotency guards protect individual requests. But when your downstream API goes down entirely, n8n keeps retrying every queued execution — burning budget and flooding a dead service. A circuit breaker detects this and routes executions to a dead-letter queue before the thundering-herd failure begins.</p>
<p style="margin-bottom: 1em; line-height: 1.9;">Add a <strong>Code</strong> node <em>before</em> your HTTP Request node:</p>
<pre style="background: #fafafa; padding: 24px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 1.8;">// Circuit Breaker — place this Code node before any external API call
const service = 'stripe_api';
const failureThreshold = 3; // open circuit after 3 failures
const windowMs = 300000; // 5-minute rolling window
// Read recent failures from n8n static data (persists across executions)
const state = $getWorkflowStaticData('global');
const now = Date.now();
// Remove failures that have aged out of the window
state[service] = (state[service] || []).filter(t => now - t < windowMs); if (state[service].length >= failureThreshold) {
// Circuit is OPEN — skip the API call
return [{
json: {
circuit: 'open',
action: 'dead_letter_queue',
retryAfter: 'manual_review',
message: `${service} circuit open — ${state[service].length} failures in last 5 min`
}
}];
}
// Circuit is CLOSED — safe to proceed
return [{ json: { ...$input.first().json, circuit: 'closed' } }];
</pre>
<p style="margin-top: 1.2em; margin-bottom: 3em; line-height: 1.9;">Then add an <strong>IF</strong> node immediately after: if <code>circuit = 'open'</code> → route to a Slack alert and stop. If <code>circuit = 'closed'</code> → proceed to the HTTP Request node with the idempotency key attached.</p>
<hr style="margin: 55px 0;" />
<p><!-- ── SECTION 4: RETRY CONFIGURATION ─────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">4. Safe Retry Configuration in n8n</h2>
<p style="margin-bottom: 1.5em; line-height: 1.9;">Follow these four steps inside each side-effect node:</p>
<ol style="margin-bottom: 2.5em; line-height: 2.2; padding-left: 1.4em;">
<li>Open the node → click the <strong>Retry</strong> tab.</li>
<li>Set <strong>Maximum Retries</strong> to <code>5</code>.</li>
<li>Set <strong>Retry Strategy</strong> to <code>Exponential Backoff</code>.</li>
<li>Enable <strong>“Skip on Success (Idempotent)”</strong> — n8n stops further retries once the node reports success <em>and</em> the guard confirms <code>completed</code>.</li>
</ol>
<h3 style="margin-bottom: 16px; line-height: 1.3;">Retry Backoff Matrix</h3>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Retry #</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Backoff (ms)</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">1</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">0</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Immediate first attempt</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">2</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">500</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">After 0.5 s</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">3</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">1,500</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">After 1.5 s</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">4</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">3,500</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">After 3.5 s</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">5</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">7,500</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Final attempt</td>
</tr>
</tbody>
</table>
<blockquote style="margin: 0 0 2em 0; padding: 16px 20px; border-left: 4px solid #e0e0e0; background: #fafafa; border-radius: 0 6px 6px 0; font-style: italic;">
<p style="margin: 0; line-height: 1.9;"><strong>Warning:</strong> Exponential backoff can cause a <strong>thundering herd</strong> when many identical workflows fail simultaneously (e.g., during a downstream API outage). Mitigate by adding a <strong>random jitter</strong> of <code>±200 ms</code> to the backoff formula.</p>
</blockquote>
<hr style="margin: 55px 0;" />
<p><!-- ── SECTION 5: DETECTING & ALERTING ────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">5. Detecting & Alerting on Idempotency Failures</h2>
<p style="margin-bottom: 1.5em; line-height: 1.9;">If you encounter any <a href="/n8n-silent-failures-no-logs">n8n silent failures with no logs</a>, resolve those before setting up alerting here.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2.5em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Tool</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Metric to watch</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Alert condition</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>n8n Execution Log</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>status = "error"</code> on node “Create Order”</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Send Slack alert if > 3 errors in 5 min.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Prometheus Exporter</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>n8n_node_retry_total{node="Create Order", result="failed"}</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Alert if rate > 0.2 rps.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Datadog</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>idempotency_log.status = "pending"</code> older than 30 min</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Trigger incident automatically.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 1em; line-height: 1.9;"><strong>Grafana dashboard query</strong> — surface pending keys older than 30 minutes:</p>
<pre style="background: #fafafa; padding: 24px; border: 1px solid #e0e0e0; border-radius: 6px; overflow: auto; font-size: 0.91em; line-height: 1.8;">SELECT
key,
status,
DATE_PART('epoch', NOW() - created_at) AS age_seconds
FROM idempotency_log
WHERE status = 'pending'
AND age_seconds > 1800;
</pre>
<hr style="margin: 55px 0;" />
<p><!-- ── ONE-LINE FIX ────────────────────────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">One‑Line Fix for Duplicate Writes</h2>
<blockquote style="margin: 0 0 3em 0; padding: 20px 24px; border-left: 4px solid #2e75b6; background: #f0f7ff; border-radius: 0 6px 6px 0; font-style: italic;">
<p style="margin: 0; line-height: 1.9;">Add an idempotency key, store it in a log table with a unique constraint, and set each node’s retry policy to “Skip on Success.” This guarantees that a second execution sees the key marked <em>completed</em> and aborts without re‑sending the external request.</p>
</blockquote>
<hr style="margin: 55px 0;" />
<p><!-- ── IF THIS → DO THIS TABLE ────────────────────────────────── --></p>
<h2 style="margin-bottom: 16px; line-height: 1.3;">⚡ If This → Do This: Quick-Fix Reference</h2>
<p style="margin-bottom: 1.5em; line-height: 1.9; color: #555;">Bookmark this table. When a duplicate incident hits at 2 AM, you want the fix in 30 seconds — not after re-reading the whole guide.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 3em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 14px; background: #f5f5f5;">If you see this…</th>
<th style="border: 1px solid #e0e0e0; padding: 14px; background: #f5f5f5;">The cause is…</th>
<th style="border: 1px solid #e0e0e0; padding: 14px; background: #f5f5f5;">Do this immediately</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Two executions, same payload, both show <code>success</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Webhook re-delivery — no idempotency gate</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Add Code node with SHA256 key + DB guard as the <strong>first</strong> node after Webhook</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">One execution shows <code>error</code> then <code>success</code> on retry</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Auto-retry fired without idempotency header</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Add <code>Idempotency-Key</code> header to HTTP Request node using a stable business key</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>idempotency_log</code> rows stuck in <code>pending</code> > 30 min</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">External API returned non-2xx; key never marked <code>completed</code></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Run Grafana query from Section 5, investigate API error, manually mark or retry</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Duplicate rows from two parallel branches</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Race condition — both branches wrote before either committed</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use PostgreSQL <code>SERIALIZABLE</code> isolation + <code>ON CONFLICT DO NOTHING</code> in both branches</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Entire downstream API is down; executions piling up</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">No circuit breaker — n8n retrying a dead service</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Add circuit breaker Code node (Section 3.5) — route to dead-letter queue until API recovers</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Manual re-run creates duplicates</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Log was cleared between runs; guard missed the <code>completed</code> row</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Never truncate <code>idempotency_log</code> without archiving. Keep rows for at least 72 hours.</td>
</tr>
</tbody>
</table>
<hr style="margin: 55px 0;" />
<p><!-- ── SECTION 6: FAQ ──────────────────────────────────────────── --></p>
<h2 style="margin-bottom: 20px; line-height: 1.3;">6. Frequently Asked Questions</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Question</th>
<th style="border: 1px solid #e0e0e0; padding: 13px; background: #f9f9f9;">Answer</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Do I need to update every node?</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Only <strong>side‑effect nodes</strong> (HTTP Request, Send Email, Database Write). Pure transformation nodes are already idempotent by nature.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>What if the external API has no idempotency support?</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Implement a <em>local guard</em>: check the log before the call and wrap it in a DB transaction that rolls back on failure.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Can I reuse the same key across different workflows?</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Yes, if the key represents a <strong>business entity</strong> (e.g., order ID). Ensure the uniqueness scope matches the downstream system’s expectations.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>How does n8n’s “Execute Workflow” node handle retries?</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">It inherits the parent workflow’s retry settings. Wrap it with the same idempotency guard to prevent nested duplicate runs.</td>
</tr>
</tbody>
</table>
<hr style="margin: 55px 0;" />
<p style="color: #888; font-size: 0.9em; line-height: 1.9;"><em>Prepared by the senior SEO & n8n technical team — authoritative, production‑ready guidance for eliminating idempotency failures.</em></p>
Step by Step Guide to solve n8n idempotency retry failures
I shipped a Stripe payment workflow on a Friday. By Monday morning, 11 customers had been charged twice. Support tickets were piling up. The workflow hadn’t crashed – it had done exactly what I built it to do: retry on failure.
The real bug wasn’t in the code. It was that I’d never made the workflow idempotent. This guide exists so you don’t have that Monday.
Who this is for: n8n developers and automation engineers who need reliable, production‑grade workflows that avoid duplicate side‑effects. We cover this in detail in the n8n Production Failure Patterns Guide.
🔍 Debug Decision Tree: Find Your Fix in 60 Seconds
Most devs get stuck here: they know something duplicated, but have no systematic way to pinpoint where the guard failed. Use this tree before doing anything else.
Are you seeing duplicate records / emails / charges?
│
├── YES — Did the same n8n execution run more than once?
│ │
│ ├── YES (check Execution Log: same execution_id ran 2×)
│ │ └── ➜ Webhook re-delivery problem
│ │ Fix: Add idempotency gate BEFORE first side-effect node
│ │ (see Section 2: Webhook Re-delivery below)
│ │
│ └── NO (different execution_ids, same payload)
│ │
│ ├── Did a node retry after a timeout / 5xx?
│ │ └── YES ➜ Missing idempotency key on HTTP Request node
│ │ Fix: Section 3.3 — attach Idempotency-Key header
│ │
│ └── Did you manually re-run the workflow?
│ └── YES ➜ No pre-call DB guard in place
│ Fix: Section 3.2 — INSERT ON CONFLICT pattern
│
└── NO — Getting "pending" status stuck in idempotency_log?
│
├── Older than 30 min → external API never returned 2xx
│ Fix: Section 5 — Grafana query to find stale keys
│
└── Race condition (two parallel branches wrote same key simultaneously)
Fix: Section 3.5 — Circuit Breaker pattern below
Quick Diagnosis
Problem: Re‑running a failed n8n workflow (or a node inside it) writes the same data twice, creates orphan records, or triggers side‑effects such as duplicate emails or payments.
Featured‑snippet solution: Add an idempotency key to every external call, store the key with a status flag, and configure the node’s Retry options to “Skip on success.” This guarantees that a second attempt sees the previous successful execution and aborts without writing again.
A node that can be executed multiple times with the same input and produce the exact same result (no extra side‑effects).
Safe retries, no duplicate records.
Non‑idempotent operation
Nodes that trigger external actions (e.g., HTTP Request, Send Email, Stripe Charge) that create resources on each call.
Duplicate emails, double‑charged payments, extra DB rows.
Idempotency key
A deterministic identifier (UUID, hash of payload, or business key) attached to the request.
Used by the downstream system to detect repeats.
Note: Many SaaS APIs (Stripe, PayPal, SendGrid) require the key in a specific header (Idempotency‑Key). If the API does not support it, implement a local guard (e.g., DB lock) to achieve the same effect.
2. Common Scenarios That Trigger Idempotency Failures
Scenario
Why it fails
Automatic retries (default 3×) on a node that creates a record
Each retry repeats the POST request → two rows in DB.
Manual re‑run of a failed workflow
The workflow starts from the first node again, replaying all side‑effects.
Parallel branches writing to the same resource
Race condition: both branches write before the other sees the commit.
Webhook re‑delivery (e.g., Stripe webhook retried after 30 s)
n8n receives the same event twice and forwards it downstream.
How Webhook Re-delivery Creates Duplicates – The Exact Flow
This is where most devs get stuck: Stripe or Shopify retries the webhook because your workflow took too long to respond. n8n treats it as a brand-new execution. No built-in deduplication kicks in.
When you don’t have a stable business key (e.g. processing raw webhook payloads), generate a collision-resistant fingerprint from the payload content instead. Add a Code node immediately after your Webhook node:
⚠️ Do not include volatile fields like Date headers, request_id, or random salts in the hash. The key must be identical across all retry attempts for the same logical event.
3.2 Store the Key Before the External Call
Choose a storage backend that matches your load:
Storage option
When to use
Considerations
n8n DB (SQLite)
Small volume, low traffic
SQLite locks can deadlock under high concurrency — add a busy_timeout.
External DB (PostgreSQL / MySQL)
High‑throughput, multi‑instance n8n
Use SERIALIZABLE isolation to avoid phantom reads.
Cache (Redis)
Fast look‑ups, TTL‑based expiry
TTL must exceed the longest possible retry window.
⚠️ Most devs get stuck here: They write the idempotency key to the DB after the external call succeeds. This is backwards. If the external call times out midway and n8n retries, the key was never written — so the guard misses it and the duplicate fires anyway.
Always write the key with status pending BEFORE the external call.
Insert‑if‑not‑exists (PostgreSQL) — this query creates a log entry only once:
INSERT INTO idempotency_log (key, status, created_at)
VALUES ($1, 'pending', NOW())
ON CONFLICT (key) DO UPDATE
SET status = EXCLUDED.status
RETURNING status;
If the returned status is completed, skip the downstream call entirely.
If you encounter any n8n long running workflow failures, resolve them before continuing.
What a duplicate-causing retry looks like in the n8n Execution Log
This log pattern is the tell-sign that idempotency is broken — two executions, same input, both showing success:
If you see two FINISHED executions with identical payloads and different ch_ IDs, your guard is either missing or placed after the charge node — not before it.
3.3 Attach the Key to the External Call
Map the key to the header expected by the target API:
API
Header name
n8n node field
Stripe
Idempotency-Key
Headers → Idempotency-Key
SendGrid
X-Message-Id
Headers → X-Message-Id
Custom REST
Idempotency-Key (or custom)
Headers → Idempotency-Key
Add the header inside the HTTP Request node configuration:
After the request returns, use a Set node to update the log record:
Outcome
Action
2xx (success)
Update idempotency_log.status = 'completed'.
4xx / 5xx (error)
Keep status = 'pending' so the next retry can attempt it again.
3.5 Circuit Breaker — Stop the Cascade Before It Starts
Idempotency guards protect individual requests. But when your downstream API goes down entirely, n8n keeps retrying every queued execution — burning budget and flooding a dead service. A circuit breaker detects this and routes executions to a dead-letter queue before the thundering-herd failure begins.
Add a Code node before your HTTP Request node:
// Circuit Breaker — place this Code node before any external API call
const service = 'stripe_api';
const failureThreshold = 3; // open circuit after 3 failures
const windowMs = 300000; // 5-minute rolling window
// Read recent failures from n8n static data (persists across executions)
const state = $getWorkflowStaticData('global');
const now = Date.now();
// Remove failures that have aged out of the window
state[service] = (state[service] || []).filter(t => now - t < windowMs); if (state[service].length >= failureThreshold) {
// Circuit is OPEN — skip the API call
return [{
json: {
circuit: 'open',
action: 'dead_letter_queue',
retryAfter: 'manual_review',
message: `${service} circuit open — ${state[service].length} failures in last 5 min`
}
}];
}
// Circuit is CLOSED — safe to proceed
return [{ json: { ...$input.first().json, circuit: 'closed' } }];
Then add an IF node immediately after: if circuit = 'open' → route to a Slack alert and stop. If circuit = 'closed' → proceed to the HTTP Request node with the idempotency key attached.
4. Safe Retry Configuration in n8n
Follow these four steps inside each side-effect node:
Open the node → click the Retry tab.
Set Maximum Retries to 5.
Set Retry Strategy to Exponential Backoff.
Enable “Skip on Success (Idempotent)” — n8n stops further retries once the node reports success and the guard confirms completed.
Retry Backoff Matrix
Retry #
Backoff (ms)
Notes
1
0
Immediate first attempt
2
500
After 0.5 s
3
1,500
After 1.5 s
4
3,500
After 3.5 s
5
7,500
Final attempt
Warning: Exponential backoff can cause a thundering herd when many identical workflows fail simultaneously (e.g., during a downstream API outage). Mitigate by adding a random jitter of ±200 ms to the backoff formula.
SELECT
key,
status,
DATE_PART('epoch', NOW() - created_at) AS age_seconds
FROM idempotency_log
WHERE status = 'pending'
AND age_seconds > 1800;
One‑Line Fix for Duplicate Writes
Add an idempotency key, store it in a log table with a unique constraint, and set each node’s retry policy to “Skip on Success.” This guarantees that a second execution sees the key marked completed and aborts without re‑sending the external request.
⚡ If This → Do This: Quick-Fix Reference
Bookmark this table. When a duplicate incident hits at 2 AM, you want the fix in 30 seconds — not after re-reading the whole guide.
If you see this…
The cause is…
Do this immediately
Two executions, same payload, both show success
Webhook re-delivery — no idempotency gate
Add Code node with SHA256 key + DB guard as the first node after Webhook
One execution shows error then success on retry
Auto-retry fired without idempotency header
Add Idempotency-Key header to HTTP Request node using a stable business key
idempotency_log rows stuck in pending > 30 min
External API returned non-2xx; key never marked completed
Run Grafana query from Section 5, investigate API error, manually mark or retry
Duplicate rows from two parallel branches
Race condition — both branches wrote before either committed
Use PostgreSQL SERIALIZABLE isolation + ON CONFLICT DO NOTHING in both branches
Entire downstream API is down; executions piling up
No circuit breaker — n8n retrying a dead service
Add circuit breaker Code node (Section 3.5) — route to dead-letter queue until API recovers
Manual re-run creates duplicates
Log was cleared between runs; guard missed the completed row
Never truncate idempotency_log without archiving. Keep rows for at least 72 hours.
6. Frequently Asked Questions
Question
Answer
Do I need to update every node?
Only side‑effect nodes (HTTP Request, Send Email, Database Write). Pure transformation nodes are already idempotent by nature.
What if the external API has no idempotency support?
Implement a local guard: check the log before the call and wrap it in a DB transaction that rolls back on failure.
Can I reuse the same key across different workflows?
Yes, if the key represents a business entity (e.g., order ID). Ensure the uniqueness scope matches the downstream system’s expectations.
How does n8n’s “Execute Workflow” node handle retries?
It inherits the parent workflow’s retry settings. Wrap it with the same idempotency guard to prevent nested duplicate runs.
Prepared by the senior SEO & n8n technical team — authoritative, production‑ready guidance for eliminating idempotency failures.