<figure class="wp-block-image aligncenter"><img src="https://flowgenius.in/wp-content/uploads/2026/01/n8n-exactly-once-execution.png" alt="Step by Step Guide to solve n8n exactly once execution" /> <figcaption style="text-align: center;">Step by Step Guide to solve n8n exactly once execution</p>
<hr />
</figcaption></figure>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Who this is for:</strong> Engineers building finance‑ or inventory‑critical automations in n8n that must avoid duplicate records. <strong>We cover this in detail in the </strong>Production‑Grade n8n Architecture.</p>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;"><p>In production duplicate records appear when a webhook retry fires after a brief network glitch.</p></blockquote>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">Quick Diagnosis</h2>
<ol style="margin-bottom: 2em; line-height: 1.9;">
<li><strong>Persist a deterministic deduplication key</strong> (e.g., <code>sourceId:timestamp</code>) in a durable store (PostgreSQL, Redis, or n8n Data Store).</li>
<li><strong>Check‑before‑process</strong> – an <em>If/Else</em> node queries the store; if the key exists → <em>skip</em>, else → <em>process</em>.</li>
<li><strong>Claim the key atomically</strong> (<code>INSERT … ON CONFLICT</code> or Redis <code>SETNX</code>) so concurrent runs cannot insert the same key.</li>
<li><strong>Mark success only after the downstream action succeeds</strong> (e.g., after a Stripe charge).</li>
<li><strong>Route failures to a dead‑letter queue</strong> to avoid silent data loss.</li>
</ol>
<p style="margin-bottom: 2em; line-height: 1.9;">Implement the pattern below and you’ll get exactly‑once semantics even if n8n restarts, retries, or scales horizontally.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">1. Exactly‑Once vs. At‑Least‑Once in n8n</h2>
<p>If you encounter any <a href="/stateless-vs-stateful-workflows-n8n">stateless vs stateful workflows n8n </a>resolve them before continuing with the setup.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Model</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Guarantees</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">n8n Default</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">When it Breaks</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>At‑Least‑Once</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Every event processed <strong>≥ 1</strong> time; duplicates possible on retries.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">All built‑in triggers (Webhook, Cron, Polling) auto‑retry on failure.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Network hiccup, timeout, or node error → retry → duplicate payload.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Exactly‑Once</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Each logical event processed <strong>once and only once</strong>.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Requires explicit idempotency handling; n8n does not provide it out‑of‑the box.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Missing dedup key, non‑transactional side‑effects, or race conditions.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA note:</em> Exactly‑once is a <strong>promise</strong> you enforce with external state. n8n’s internal queue is volatile, so you need a durable store. Most teams notice at‑least‑once behaviour on the first retry.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">2. Choosing a Durable Deduplication Store</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Store</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Pros</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Cons</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Ideal Use‑Case</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>PostgreSQL (or any RDBMS)</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Strong ACID guarantees; <code>SELECT … FOR UPDATE</code> available.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Adds a DB round‑trip latency.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Pipelines that already use a relational DB.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Redis (<code>SETNX</code> / Lua)</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">In‑memory speed; atomic claim.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Data loss if persistence (<code>AOF</code>) isn’t enabled.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">High‑throughput webhook ingestion.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>n8n Data Store (v0.210+)</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">No external dependency; UI‑driven.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Limited payload size; no native transactions.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Low‑volume internal automations.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;">Implementation tip: Build a *composite key* from immutable source fields, e.g., <code>order:12345:2024-09-01T12:00:00Z</code>. Store a simple flag (<code>processed = true</code>) and optionally a timestamp for audit.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">3. Blueprint: Exactly‑Once Sub‑Workflow</h2>
<p>If you encounter any <a href="/n8n-orchestration-vs-execution-engine">n8n orchestration vs execution engine </a>resolve them before continuing with the setup.</p>
<blockquote style="margin: 0 0 2em 0; padding-left: 1em; border-left: 4px solid #e0e0e0;"><p><strong>Purpose:</strong> A reusable sub‑workflow (<code>ExactOnceTemplate</code>) that you can embed via the *Execute Workflow* node.</p></blockquote>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.1. Node Flow Overview</h3>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Step</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Node</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Role</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">1</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Webhook Trigger</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Receives raw payload.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">2</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Set → <code>dedupKey</code></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Computes a deterministic key.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">3</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Claim Key</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Atomically inserts the key (Postgres or Redis).</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">4</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">If/Else</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Skips if key already exists.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">5</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Business Logic</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Your core processing (e.g., create invoice).</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">6</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Mark Completed</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Updates the store to <code>completed</code>.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">7</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Dead‑Letter</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Captures failures for later replay.</td>
</tr>
</tbody>
</table>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.2. PostgreSQL – Table Definition</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*Create the dedup table once (run outside the workflow).*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">CREATE TABLE n8n_dedup (
dedup_key TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT NOW(),
status TEXT NOT NULL DEFAULT 'claimed' -- claimed | completed | failed
);
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.3. PostgreSQL – Claim the Key</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*This query runs in the **Claim Key** node. It attempts to insert the key; if it already exists, nothing is inserted.*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">INSERT INTO n8n_dedup (dedup_key)
VALUES ({{ $json.dedupKey }})
ON CONFLICT (dedup_key) DO NOTHING
RETURNING *;
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">– <strong>Result > 0 rows</strong> → key was newly claimed → continue.<br />
– <strong>Result = 0 rows</strong> → key already present → skip processing.<br />
– *At this point, regenerating the key is usually faster than chasing edge cases.*</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.4. Redis – Atomic Claim via <code>SETNX</code></h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*If you prefer Redis, use an HTTP request to a proxy that runs <code>SETNX</code>. The response tells you whether you own the key.*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">POST https://my-redis.example.com/setnx
Content-Type: application/json
{
"key": "{{ $json.dedupKey }}",
"value": "claimed",
"expire": 86400 // 1‑day TTL
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">– **Response `1`** → key acquired, proceed.<br />
– **Response `0`** → key exists, skip.<br />
*EEFA tip:* Wrap the <code>SETNX</code> and TTL in a Lua script to guarantee atomicity.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.5. If/Else – Decide Whether to Process</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*The condition checks the length of the claim node’s output.*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"value1": "={{$node[\"Claim Key\"].json.length}}",
"operation": "greaterThan",
"value2": 0
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">– **True** → key claimed → take the **Process** branch.<br />
– **False** → duplicate → take the **Skip** branch.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.6. Business Logic Example – Stripe Charge</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*Only runs when the key is newly claimed.*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"url": "https://api.stripe.com/v1/charges",
"method": "POST",
"headerAuth": {
"name": "Authorization",
"value": "Bearer {{ $env.STRIPE_SECRET_KEY }}"
},
"bodyParametersUi": {
"parameter": [
{ "name": "amount", "value": "={{$json.amount * 100}}" },
{ "name": "currency", "value": "usd" },
{ "name": "source", "value": "={{$json.token}}" },
{ "name": "description", "value": "Order {{ $json.orderId }}" }
]
},
"options": {
"jsonContentType": true,
"idempotencyKey": "{{ $json.dedupKey }}"
}
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">The Stripe <code>Idempotency-Key</code> mirrors our dedup key, providing a second line of defense.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.7. Mark Completed – Update Store</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*Runs after a successful Stripe response.*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">UPDATE n8n_dedup
SET status = 'completed',
external_id = '{{ $node["Stripe Charge"].json.id }}'
WHERE dedup_key = '{{ $json.dedupKey }}';
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">If the update fails, n8n’s error workflow will roll back the transaction.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.8. Dead‑Letter Queue – Capture Failures</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">*If the Stripe call throws, forward the original payload and error details to a DLQ endpoint.*</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"url": "https://my-dlq.example.com/record",
"method": "POST",
"jsonParameters": true,
"bodyParametersUi": {
"parameter": [
{ "name": "payload", "value": "={{$json}}" },
{ "name": "error", "value": "={{$node[\"Stripe Charge\"].error}}" }
]
}
}
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">In practice, failed payloads sit in a dead‑letter queue for a few hours before an engineer replays them.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">4. Transactional Wrapper (Two‑Phase Commit)</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">When the downstream API is not idempotent, wrap the whole sequence in a DB transaction.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Phase</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">1️⃣ Reserve</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Insert dedup key with status <code>claimed</code>.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">2️⃣ Execute</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Call external API (e.g., Stripe).</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">3️⃣ Commit</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Update row to <code>completed</code> and store external ID.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">4️⃣ Rollback (on failure)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Set status to <code>failed</code>; optional compensation logic.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;">**If your downstream service can’t guarantee idempotency, you need the extra safety net of a two‑phase commit.**</p>
<p style="margin-bottom: 2em; line-height: 1.9;">**Postgres transaction example (single node using <code>Execute Query</code>):**</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">BEGIN;
INSERT INTO n8n_dedup (dedup_key, status)
VALUES ({{ $json.dedupKey }}, 'claimed')
ON CONFLICT (dedup_key) DO UPDATE
SET status = 'claimed'
WHERE status <> 'completed';
-- Assume $node["Stripe Charge"].json.id holds the external ID
UPDATE n8n_dedup
SET status = 'completed', external_id = '{{ $node["Stripe Charge"].json.id }}'
WHERE dedup_key = '{{ $json.dedupKey }}';
COMMIT;
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">If any step throws, n8n’s error handling aborts the transaction automatically. If you encounter any <a href="/event-driven-vs-batch-n8n">event driven vs batch n8n </a>resolve them before continuing with the setup.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">5. Idempotent Design Checklist</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Steps</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Item</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Reason</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">1</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Deterministic dedup key</strong> (source‑ID + timestamp)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Guarantees identical events map to the same key.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">2</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Atomic claim</strong> (<code>INSERT … ON CONFLICT</code> or <code>SETNX</code>)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Prevents two workers from processing the same event.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">3</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Transaction around side‑effects</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Ensures “all‑or‑nothing” state.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">4</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Idempotent downstream APIs</strong> (support <code>Idempotency-Key</code>)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Even a stray duplicate is ignored by the API.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">5</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>TTL on dedup entries</strong> (24‑48 h)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Cleans up storage while still protecting against retries.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">6</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Dead‑letter queue</strong></td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Enables manual replay without data loss.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">7</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Monitoring & alerts</strong> (duplicate‑skip counter)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Detects unexpected high duplicate rates.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">8</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Race‑condition testing</strong> (parallel webhook calls)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Validates atomic claim logic.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;">Copy‑paste this table into your internal docs for quick reference.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">6. Real‑World Pitfalls & Mitigations</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Pitfall</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Symptom</th>
<th style="padding: 13px; border: 1px solid #e0e0e0;">Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Non‑deterministic key (e.g., random UUID)</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Every run looks new → duplicates.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Derive key from immutable source fields.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Missing TTL</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Dedup table grows indefinitely, slowing queries.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Set an expiration (`expire` in Redis or `ON DELETE` policy in Postgres).</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Side‑effect before claim</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">API call succeeds, claim fails → duplicate external action.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;"><strong>Claim first</strong>, then call the external service.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Concurrent workers without locking</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Two workers insert same key → both succeed.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Use DB‑level atomic <code>INSERT … ON CONFLICT</code> or Redis <code>SETNX</code>.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Ignoring API idempotency</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">External service creates duplicate records.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Pass the same <code>Idempotency-Key</code> header to the API.</td>
</tr>
<tr>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Error path leaves claim as “claimed”</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">Subsequent retries are blocked forever.</td>
<td style="padding: 13px; border: 1px solid #e0e0e0;">On error, set status to <code>failed</code> or delete the key after a back‑off.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;">A quick alert on duplicate skips usually catches mis‑configurations before they affect customers.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">7. Monitoring & Alerting Blueprint</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">7.1. Prometheus Alert for Duplicate Skips</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">- alert: N8N_DuplicateSkipped
expr: sum(rate(n8n_workflow_executions_total{status="skipped",reason="duplicate"}[5m])) > 5
for: 2m
labels:
severity: warning
annotations:
summary: "High rate of duplicate order skips in n8n"
description: "More than 5 duplicate skips per minute detected. Verify dedup store health."
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">7.2. Grafana Panels</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">– **Completed vs. Skipped Rate**: <code>rate(n8n_workflow_executions_total{status="completed"}[1m])</code> vs. <code>rate(...{status="skipped"})</code>.<br />
– **Error Spike**: <code>rate(n8n_workflow_executions_total{status="error"}[5m])</code>.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">7.3. Slack Notification</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Configure a webhook that posts on the <code>N8N_DuplicateSkipped</code> alert to the **#n8n-reliability** channel.</p>
<hr style="margin: 55px 0; border: none; height: 1px; background: #e0e0e0;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">Conclusion</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">By persisting a <strong>deterministic deduplication key</strong>, claiming it atomically, and wrapping every side‑effect in a transaction, you turn any n8n workflow from “at‑least‑once” into <strong>exactly‑once</strong>. The pattern works with PostgreSQL, Redis, or n8n’s built‑in Data Store, scales horizontally, and survives restarts—delivering the data‑consistency guarantees required by finance, inventory, and compliance‑critical automations. Implement it once, and your workflows will reliably process each logical event <strong>once and only once</strong>.</p>
Step by Step Guide to solve n8n exactly once execution
Who this is for: Engineers building finance‑ or inventory‑critical automations in n8n that must avoid duplicate records. We cover this in detail in the Production‑Grade n8n Architecture.
In production duplicate records appear when a webhook retry fires after a brief network glitch.
Quick Diagnosis
Persist a deterministic deduplication key (e.g., sourceId:timestamp) in a durable store (PostgreSQL, Redis, or n8n Data Store).
Check‑before‑process – an If/Else node queries the store; if the key exists → skip, else → process.
Claim the key atomically (INSERT … ON CONFLICT or Redis SETNX) so concurrent runs cannot insert the same key.
Mark success only after the downstream action succeeds (e.g., after a Stripe charge).
Route failures to a dead‑letter queue to avoid silent data loss.
Implement the pattern below and you’ll get exactly‑once semantics even if n8n restarts, retries, or scales horizontally.
Requires explicit idempotency handling; n8n does not provide it out‑of‑the box.
Missing dedup key, non‑transactional side‑effects, or race conditions.
EEFA note: Exactly‑once is a promise you enforce with external state. n8n’s internal queue is volatile, so you need a durable store. Most teams notice at‑least‑once behaviour on the first retry.
2. Choosing a Durable Deduplication Store
Store
Pros
Cons
Ideal Use‑Case
PostgreSQL (or any RDBMS)
Strong ACID guarantees; SELECT … FOR UPDATE available.
Adds a DB round‑trip latency.
Pipelines that already use a relational DB.
Redis (SETNX / Lua)
In‑memory speed; atomic claim.
Data loss if persistence (AOF) isn’t enabled.
High‑throughput webhook ingestion.
n8n Data Store (v0.210+)
No external dependency; UI‑driven.
Limited payload size; no native transactions.
Low‑volume internal automations.
Implementation tip: Build a *composite key* from immutable source fields, e.g., order:12345:2024-09-01T12:00:00Z. Store a simple flag (processed = true) and optionally a timestamp for audit.
Purpose: A reusable sub‑workflow (ExactOnceTemplate) that you can embed via the *Execute Workflow* node.
3.1. Node Flow Overview
Step
Node
Role
1
Webhook Trigger
Receives raw payload.
2
Set → dedupKey
Computes a deterministic key.
3
Claim Key
Atomically inserts the key (Postgres or Redis).
4
If/Else
Skips if key already exists.
5
Business Logic
Your core processing (e.g., create invoice).
6
Mark Completed
Updates the store to completed.
7
Dead‑Letter
Captures failures for later replay.
3.2. PostgreSQL – Table Definition
*Create the dedup table once (run outside the workflow).*
CREATE TABLE n8n_dedup (
dedup_key TEXT PRIMARY KEY,
created_at TIMESTAMP DEFAULT NOW(),
status TEXT NOT NULL DEFAULT 'claimed' -- claimed | completed | failed
);
3.3. PostgreSQL – Claim the Key
*This query runs in the **Claim Key** node. It attempts to insert the key; if it already exists, nothing is inserted.*
INSERT INTO n8n_dedup (dedup_key)
VALUES ({{ $json.dedupKey }})
ON CONFLICT (dedup_key) DO NOTHING
RETURNING *;
– Result > 0 rows → key was newly claimed → continue.
– Result = 0 rows → key already present → skip processing.
– *At this point, regenerating the key is usually faster than chasing edge cases.*
3.4. Redis – Atomic Claim via SETNX
*If you prefer Redis, use an HTTP request to a proxy that runs SETNX. The response tells you whether you own the key.*
In practice, failed payloads sit in a dead‑letter queue for a few hours before an engineer replays them.
4. Transactional Wrapper (Two‑Phase Commit)
When the downstream API is not idempotent, wrap the whole sequence in a DB transaction.
Phase
Action
1️⃣ Reserve
Insert dedup key with status claimed.
2️⃣ Execute
Call external API (e.g., Stripe).
3️⃣ Commit
Update row to completed and store external ID.
4️⃣ Rollback (on failure)
Set status to failed; optional compensation logic.
**If your downstream service can’t guarantee idempotency, you need the extra safety net of a two‑phase commit.**
**Postgres transaction example (single node using Execute Query):**
BEGIN;
INSERT INTO n8n_dedup (dedup_key, status)
VALUES ({{ $json.dedupKey }}, 'claimed')
ON CONFLICT (dedup_key) DO UPDATE
SET status = 'claimed'
WHERE status <> 'completed';
-- Assume $node["Stripe Charge"].json.id holds the external ID
UPDATE n8n_dedup
SET status = 'completed', external_id = '{{ $node["Stripe Charge"].json.id }}'
WHERE dedup_key = '{{ $json.dedupKey }}';
COMMIT;
If any step throws, n8n’s error handling aborts the transaction automatically. If you encounter any event driven vs batch n8n resolve them before continuing with the setup.
5. Idempotent Design Checklist
Steps
Item
Reason
1
Deterministic dedup key (source‑ID + timestamp)
Guarantees identical events map to the same key.
2
Atomic claim (INSERT … ON CONFLICT or SETNX)
Prevents two workers from processing the same event.
Cleans up storage while still protecting against retries.
6
Dead‑letter queue
Enables manual replay without data loss.
7
Monitoring & alerts (duplicate‑skip counter)
Detects unexpected high duplicate rates.
8
Race‑condition testing (parallel webhook calls)
Validates atomic claim logic.
Copy‑paste this table into your internal docs for quick reference.
6. Real‑World Pitfalls & Mitigations
Pitfall
Symptom
Fix
Non‑deterministic key (e.g., random UUID)
Every run looks new → duplicates.
Derive key from immutable source fields.
Missing TTL
Dedup table grows indefinitely, slowing queries.
Set an expiration (`expire` in Redis or `ON DELETE` policy in Postgres).
Side‑effect before claim
API call succeeds, claim fails → duplicate external action.
Claim first, then call the external service.
Concurrent workers without locking
Two workers insert same key → both succeed.
Use DB‑level atomic INSERT … ON CONFLICT or Redis SETNX.
Ignoring API idempotency
External service creates duplicate records.
Pass the same Idempotency-Key header to the API.
Error path leaves claim as “claimed”
Subsequent retries are blocked forever.
On error, set status to failed or delete the key after a back‑off.
A quick alert on duplicate skips usually catches mis‑configurations before they affect customers.
7. Monitoring & Alerting Blueprint
7.1. Prometheus Alert for Duplicate Skips
- alert: N8N_DuplicateSkipped
expr: sum(rate(n8n_workflow_executions_total{status="skipped",reason="duplicate"}[5m])) > 5
for: 2m
labels:
severity: warning
annotations:
summary: "High rate of duplicate order skips in n8n"
description: "More than 5 duplicate skips per minute detected. Verify dedup store health."
7.2. Grafana Panels
– **Completed vs. Skipped Rate**: rate(n8n_workflow_executions_total{status="completed"}[1m]) vs. rate(...{status="skipped"}).
– **Error Spike**: rate(n8n_workflow_executions_total{status="error"}[5m]).
7.3. Slack Notification
Configure a webhook that posts on the N8N_DuplicateSkipped alert to the **#n8n-reliability** channel.
Conclusion
By persisting a deterministic deduplication key, claiming it atomically, and wrapping every side‑effect in a transaction, you turn any n8n workflow from “at‑least‑once” into exactly‑once. The pattern works with PostgreSQL, Redis, or n8n’s built‑in Data Store, scales horizontally, and survives restarts—delivering the data‑consistency guarantees required by finance, inventory, and compliance‑critical automations. Implement it once, and your workflows will reliably process each logical event once and only once.