
How to Stop Duplicate Executions in n8n (2026)
Stop duplicate n8n executions with an idempotency gate: pull a stable event key out of the payload, then claim that key in one atomic database write before any node that writes, charges, or sends. In Postgres that is INSERT ... ON CONFLICT DO NOTHING RETURNING id; in Redis it is SET key 1 NX EX 86400. One row back means you are first. Zero rows means it is a duplicate, and you stop. The Remove Duplicates node cannot do this — it is a filter, not a lock.
Why Your Workflow Ran Twice
The first instinct is that n8n glitched. Usually it didn't. There are four causes and only one of them lives inside n8n:
1. The sender retried.Webhook delivery is at-least-once by design. Stripe, GitHub, Shopify, your CRM — if they don't get a fast 2xx they assume the delivery failed and send the same event again. Your workflow ran perfectly the first time. The sender just never heard about it.
2. You told it to retry. Retry On Fail is a per-node setting and it is genuinely useful, right up until you put it on a node that creates something. A flaky API call that times out after the record was already written, retried three times, is three records.
3. The upstream system emitted it twice.Double-clicked submit buttons, a form embedded on two pages, a sync job that replays yesterday's events. This one has nothing to do with your automation stack at all.
4. The webhook got registered twice. This is the rare genuine n8n case, and it has an unmistakable signature: two executions of the same payload landing within tens of milliseconds of each other, far too close for a network retry. Community reports put the gap in the 30–70ms range. Deleting and recreating the trigger usually clears it.
The useful reframe:
You are not going to eliminate duplicate deliveries. Three of those four causes are outside your control and always will be. What you can eliminate is duplicate side effects — and that is a completely different engineering problem with a well-understood answer.
The Free Fix: Respond Immediately
Before you build anything, check one setting. The Webhook node gives you three response modes: Immediately, which fires back “Workflow got started” the moment the request lands; When Last Node Finishes, which holds the connection open for the entire run; and Using ‘Respond to Webhook’ Node, which puts the timing in your hands.
The default of holding the connection is where a huge share of duplicate executions are born. Your workflow calls an AI model, waits on a slow CRM, writes three rows, and takes eleven seconds. The sender gave up at five. It got a timeout, marked the delivery failed, and queued a retry — while your first run was still happily finishing. Nothing errored. You just built a duplicate factory.
Switch to Immediately, or respond early with a Respond to Webhook node and do the slow work afterwards. It costs you nothing and it removes the single most common cause. It does not, however, make your workflow safe — it only makes retries rarer. For that you need a gate.
Why the Remove Duplicates Node Is Not Enough
Every forum thread on this eventually points at the Remove Duplicates node, specifically its Remove Items Processed in Previous Executions mode. It keeps a history of keys it has seen and drops anything it recognises. That sounds exactly like what you want, and for a low-volume workflow that fires once a minute it is fine.
It falls over in two specific ways, and both of them show up precisely when duplicates matter most.
It checks, then it writes — and something can happen in between. Read the history, decide the key is new, record the key, continue. Four steps, not one. If the same event is being processed twice at the same moment, both executions can read the history before either has written to it, and both conclude they are the original. This is the textbook check-then-act race, and it is not a hypothetical: it is the normal state of affairs the second you run queue mode with multiple workers, where two workers on two machines are pulling from the same job queue. The docs make no atomicity claim for this node, and you should not read one into it.
The history is a rolling window.History Size defaults to 10,000 items. On a workflow doing real volume, a retry that arrives an hour late can find that its key has already aged out — and an evicted key is a key that looks brand new. You can raise the ceiling, but you are tuning a cache to do a database's job.
It is a convenience filter. Treat it as one.
The Idempotency Gate That Actually Holds
The whole trick is to stop asking “have I seen this before?” and start asking “did I just win the right to process this?” — because the second question can be answered by the database in a single operation, and single operations cannot race.
Step 1: pick a key that survives the retry. Use whatever identifier the sender already stamps on the event — event.id from Stripe, a delivery id, a message id, an order number. The retry carries the same value; that is the entire mechanism. Do not use a timestamp, a UUID you generate in a Set node, or the n8n execution id. All three change on every attempt, and every duplicate will walk straight through your gate. If the payload truly has no stable id, hash a canonical subset of the meaningful fields and be deliberate about which ones you include.
Step 2: claim it in one write. Create the table once:
CREATE TABLE idempotency_keys ( key text PRIMARY KEY, workflow text NOT NULL, status text NOT NULL DEFAULT 'in_progress', created_at timestamptz NOT NULL DEFAULT now() );
Then make the very first Postgres node in your workflow run this, and nothing else:
INSERT INTO idempotency_keys (key, workflow) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING RETURNING key;
One row back means you are the original. Zero rows means someone else already claimed it. That is the whole gate. The uniqueness check and the claim are the same statement, executed under the same lock, so there is no window for a second execution to slip through — no matter how many workers you run or how close together the retries land. Postgres's ON CONFLICT clause exists for exactly this.
If you would rather use Redis, the equivalent is one command:
SET idem:{workflow}:{event_id} 1 NX EX 86400
# OK -> you claimed it, proceed
# nil -> duplicate, stopNX is the atomic part and EX handles expiry for free. I compared the two stores for agent memory in Postgres vs Redis for n8n agent memory, and the reasoning here is the same: Postgres if you want the record to persist and be queryable, Redis if you want speed and automatic cleanup.
Step 3: put an IF node right behind it, and put both in front of everything else. Empty result goes to a dead end that responds 200 and stops. This is the part people get wrong even when the SQL is perfect — the gate has to sit before the first node that writes, charges, emails, or posts. A gate in the middle of a workflow protects the second half and leaves the first half wide open.
The Failure Mode Nobody Warns You About
Here is the trap in the naive version. You claim the key at node one. At node six the API you're calling throws a 503 and the execution dies. The sender does the right thing and retries — and your gate, working exactly as designed, rejects it, because the key is already claimed.
You have not fixed duplicates. You have converted them into silent data loss, which is strictly worse, because a duplicate row is visible and a missing one is not.
The fix is to treat the claim as two phases. Insert with status = 'in_progress', and on the final node of the happy path update it to done. Then the gate's decision has three branches instead of two:
no row -> first attempt, proceed status = done -> real duplicate, stop status = in_progress and older than 10 min -> stale claim, take it over and recent -> a twin is running, stop
Then wire an error workflow that deletes the claim whenever the run fails. Between the error handler and the staleness timeout, a crashed execution always frees its own key, and the retry that follows does the work instead of being silently swallowed.
That timeout is a real judgement call, not a default to copy. It has to be longer than your slowest legitimate run and shorter than your patience for a stuck record. Ten minutes suits webhook workflows that finish in seconds. An agent workflow that legitimately runs for four minutes needs a much wider window.
Which Gate Should You Use?
| Approach | Race-safe | Survives restart | Use it when |
|---|---|---|---|
| Remove Duplicates node | No | Rolling 10k window | Low volume, single worker, nothing costly downstream |
| Postgres ON CONFLICT | Yes | Yes | Default choice if you self-host — the database is already there |
| Redis SET NX EX | Yes | Until TTL | High volume, queue mode, you want claims to expire themselves |
| Unique constraint on the target table | Yes | Yes | The workflow only writes one row and you can catch the conflict |
That last row is worth a second look, because it is the cheapest option on the list. If your workflow's only side effect is inserting one record, you do not need a separate gate at all — put a UNIQUE constraint on the natural key of the target table and let the second insert fail. The database is already the arbiter. Adding a gate in front of it is ceremony.
What This Costs in Practice
My instance runs 179 workflows, 61 of them active, at roughly 5,600 executions a month, and the median execution finishes in under a second. The gate adds one indexed write to a Postgres instance sitting on the same box — call it a couple of milliseconds against a run measured in hundreds.
The honest cost is not latency, it is discipline. Every workflow that touches money, sends a message, or writes to a system of record needs the gate, and the day you add a new one is the day you have to remember to add it again. I keep a template workflow with the gate already wired so it is a copy rather than a decision, which pairs well with keeping the definitions in version control without paying for Enterprise.
One more thing worth knowing, especially if you are near a plan limit: a rejected duplicate is still a billed execution. The gate stops the damage, not the count. If duplicates are a meaningful slice of your volume, that is a real line item — and I worked through where those limits actually bite in n8n Cloud vs self-hosted. The cheaper fix for a noisy sender is usually to stop the retries at the edge rather than inside n8n, which is the same layer where Cloudflare Access sits in front of your webhooks.
If you do one thing today:
Open your highest-stakes workflow — the one that charges a card, sends an invoice, or creates a deal — and check two things. Is the Webhook node responding immediately? And is there an atomic claim before the first node that changes anything? If either answer is no, you are relying on luck, and luck is not a delivery guarantee.
Frequently Asked Questions
Why does my n8n webhook fire twice?
Almost always because the sender retried. Webhook delivery is at-least-once by design: if the sender does not get a fast 2xx it assumes the delivery failed and sends the same event again. The most common trigger is leaving the Webhook node set to respond when the last node finishes, so a workflow that takes eight seconds blows past the sender's timeout even though it completed perfectly. Set the response to Immediately and a large share of duplicates disappear. The remaining causes are your own Retry On Fail setting on a node that creates records, an upstream system that genuinely emits the same event twice, and, rarely, a duplicated webhook registration inside n8n itself, which shows up as two executions milliseconds apart.
Is the Remove Duplicates node enough to prevent duplicate executions?
Not on its own. The Remove Duplicates node is a filter, not a lock. Its Remove Items Processed in Previous Executions mode checks a history database and then records the key, and those are two separate operations. If the same event arrives twice in parallel, which is exactly what a retry storm or a queue mode setup with several workers produces, both executions can read the history before either one writes to it, and both continue. The node also keeps a rolling history that defaults to 10,000 items, so a late retry on a busy workflow can find that its key has already been evicted. It is a reasonable convenience filter for serial, low-volume workflows and nothing more.
What should I use as an idempotency key in n8n?
Use the identifier the sender already assigns to the event: a Stripe event id, a webhook delivery id, a message id, an order number. A retry carries the same value, which is precisely what makes the gate work. Do not use a timestamp, a UUID you generate inside the workflow, or the n8n execution id, because all three are different on every attempt and every duplicate will sail straight through. If the payload genuinely has no stable identifier, hash a canonical subset of the meaningful fields, and be deliberate about which fields you include, because anything volatile makes two copies of the same event look different.
Should I store idempotency keys in Postgres or Redis?
Use Postgres if you already run it, which you do if you self-host n8n, because INSERT ON CONFLICT DO NOTHING gives you an atomic claim with no extra infrastructure and the record survives a restart. Use Redis when your volume is high enough that you care about a few milliseconds per execution and you are happy for claims to expire on their own, since SET NX EX handles both the atomic claim and the TTL in one command. The wrong answer is either an in-memory store or a two-step check-then-write against anything, because that is the race condition you were trying to fix.
Does an idempotency gate slow down my n8n workflow?
By a few milliseconds. The gate is one indexed write against a database your instance is already talking to, and on a local Postgres that is comfortably under five milliseconds. For context, the median execution on my own instance finishes in under a second, so the gate is a rounding error against the run it protects. Compare that to the cost of the failure it prevents: a duplicate charge, a double-sent email, or two rows in a CRM that someone has to reconcile by hand later.
Found duplicates in production and need them gone this week?
I build and run n8n stacks that have to survive unattended — queue mode, Postgres, real error handling, and gates in front of anything that spends money. If you have a workflow quietly doing things twice, I can audit it and wire the fix.
Related Posts
n8n
Scaling Self-Hosted n8n: When to Switch to Queue Mode (2026)
Default n8n runs the editor, webhooks, and every execution in one Node process — it works until the UI crawls during runs and webhooks drop under load. The signal to move is the main process pinned near 80% CPU; the fix is queue mode: a main instance, a Redis broker, and dedicated workers on Postgres. The exact signals I watch, the env vars I set, and the mistakes that cost me a night of dropped executions.
n8n
n8n AI Agent Memory: Postgres vs Redis (What I Run in Production)
Postgres or Redis for n8n AI agent memory? Default to Postgres Chat Memory for durable, queryable history; add Redis only when you truly need fast session context at concurrency. My decision rule, a head-to-head table, and the session-key bug that breaks memory more often than the database ever does.
n8n
n8n Cloud vs Self-Hosted in 2026: The Real Math
n8n removed active-workflow limits from every plan in 2026, so the old reason to self-host is gone. What you buy now is a monthly execution allowance and a concurrency ceiling — and the jump from Pro to Business is 4x the executions for 13x the price. Measured numbers from my own instance: 179 workflows, ~5,600 executions a month, a median run under one second, and why AI agent workflows break the concurrency assumption entirely.