
n8n webhook duplicate executions almost never mean n8n is broken. The sender delivered the same event twice, usually because your workflow answered too slowly or returned an error, and n8n correctly ran each delivery as its own execution. The fix has two parts: answer the webhook fast so fewer retries happen, then put an idempotency gate (a database row keyed on the event ID with a unique constraint) in front of anything that charges, emails or creates records, so the retries that still happen are stopped cold.
We've built 200+ n8n workflows for clients, and duplicate webhooks are one of the most common "the automation is doing something weird" tickets we get. They're also one of the most expensive, because the side effects are real: a lead gets two welcome texts, a CRM gets two contacts, a client gets charged twice.
Why does an n8n webhook trigger twice?
Webhooks run on at-least-once delivery. The sender promises your event will arrive. It doesn't promise it'll arrive once. If it doesn't get a 2xx response within its timeout, it assumes the delivery failed and tries again.
From n8n's side, that retry is just another HTTP request to the same path. There's no built-in memory that says "I've seen this one." So you get two executions, sometimes three, and every node after the trigger runs every time.
The causes we see, roughly from most to least common:
- Slow response (most common). The Webhook node is set to respond when the last node finishes, the workflow calls an LLM or a slow CRM, and the sender gives up after 5 to 30 seconds and retries while the first execution is still running.
- Error responses. A node fails, the webhook returns a 500, the sender retries. This one's sneaky because the first execution may have completed half its side effects before failing.
- Two real events. A form tool fires on "submitted" and on "updated", or two integrations post to the same URL. These aren't retries, and a dedupe gate won't always catch them because the IDs differ.
- Infrastructure. A load balancer or proxy retrying upstream, or a test and production workflow listening on the same path across two instances.
Before you fix anything, find out which of these you have. The fixes aren't the same.
How do you confirm it's a retry and not two events?
Open both executions in the n8n Executions list and compare the trigger output side by side. You're looking for three things:
- The event ID. Stripe sends an
idstarting withevt_, Meta lead webhooks carry aleadgen_id, most form tools include a submission ID. Same ID in both executions means it's a retry. - The timing. Retries from a timeout usually land 5 to 60 seconds apart. Executions 30 to 70 milliseconds apart with the same ID point to a proxy or a double-subscribed sender, not a timeout.
- Delivery headers. Some providers add an attempt counter or retry header. If it's there, it settles the question.
If the IDs are different, stop here and fix the source. Check the sender's webhook settings for duplicate subscriptions, and search your n8n instance for a second workflow using the same webhook path.
Does responding immediately fix duplicate webhooks?
It fixes the most common cause, so do it first. In the Webhook node, set Respond to Immediately. The sender gets a 200 as soon as n8n receives the request, and everything else runs after.
Here's the typical pattern. A form-to-CRM workflow enriches every lead, scores it with an LLM, then creates the contact. On a slow day for the LLM API, runs take 12 to 20 seconds. If the form tool times out at 10, it retries, and some share of leads land in the CRM twice, so sales calls the same person twice. Switching the webhook to respond immediately removes most of those duplicates straight away.
Almost zero isn't zero, though. Responding fast doesn't help when:
- n8n is restarting or the queue is backed up and the request itself times out.
- The sender retries on its own schedule for reasons you can't see.
- A node errors after you've already responded, and someone replays the event manually.
If the caller genuinely needs a result in the response, keep the heavy work out of the request path. Respond early with a 200, then hand the payload to a sub-workflow or a queue. We covered the worker side of that in how many n8n queue mode workers you actually need.
Does the Remove Duplicates node stop retries?
This is where a lot of advice online is out of date. You'll read that Remove Duplicates only works inside a single execution. That used to be true. The current node has an operation called "Remove Items Processed in Previous Executions" that stores values between runs, keeps 10,000 items by default, and can be scoped to the node or the whole workflow. The n8n Remove Duplicates docs cover the options.
So it helps. It's fine for polling workflows, like "only process RSS items I haven't seen," where executions run one at a time.
For webhooks, it isn't a lock. Two deliveries of the same event can arrive a few milliseconds apart and both run the check before either one records its value. Both pass. In queue mode with several workers, that race gets more likely, not less. You've also got a hard cap: once you've seen more than 10,000 values, the oldest drop off and a late retry can slip through.
Our rule: Remove Duplicates for convenience, a database constraint for anything with money or customer contact attached.
How do you build an idempotency gate in n8n?
An idempotency gate is one table and one query, placed right after the trigger and before any side effect. The database does the hard part, because a unique constraint is atomic: if two executions try to insert the same key at the same moment, exactly one wins.
The table, in Postgres:
create table webhook_events (
event_key text primary key,
source text not null,
received_at timestamptz not null default now()
);
The workflow:
- Webhook node, responding immediately.
- Set node that builds the key, for example
stripe:{{ $json.body.id }}ormeta_lead:{{ $json.body.entry[0].changes[0].value.leadgen_id }}. Prefix it with the source so IDs from different tools can't collide. - Postgres node running:
insert into webhook_events (event_key, source)
values ($1, $2)
on conflict (event_key) do nothing
returning event_key;
- If node checking whether a row came back. Row returned: first delivery, continue. No row: duplicate, stop (or log and stop).
That's it. It holds under concurrency, it survives restarts, and it has no 10,000-item ceiling. If you already use n8n Data Tables for small state, they're handy for plenty of things, but for this job we want a real unique constraint. Our breakdown of n8n Data Tables vs an external database explains where that line sits.
Two details people get wrong:
- Don't hash the whole body as your key. Some providers change a timestamp or attempt field on every retry, so the hash changes and every retry looks new. Use the provider's own event or object ID.
- Keep keys longer than the retry window. Stripe's webhook docs say live-mode events are retried for up to three days. We keep keys for 30 days and prune with a nightly delete. The table stays tiny.
What if the workflow fails after the gate?
This is the edge case that bites teams who do everything else right. The gate records the key, then the CRM node fails. The sender retries, the gate says "already seen," and the event is silently dropped. You've traded duplicates for lost leads.
We handle it with a status column:
- Insert the key with status
processing. - Update it to
doneas the last step of the workflow. - On conflict, check the existing row. If it's
done, stop. If it'sprocessingand older than a few minutes, treat the earlier run as dead and let this one through.
Pair that with an error workflow that alerts you when a run fails after the gate, so a human looks at it. Our guide to n8n error handling and monitoring shows the alert setup we use, and if executions are hanging rather than failing, see fixing n8n executions stuck on running.
Should the downstream actions be idempotent too?
Yes. The gate is your main defence, but belt and braces is cheap:
- Pass idempotency keys to APIs that support them. Stripe accepts an
Idempotency-Keyheader, so reuse the webhook event ID when creating a charge or refund from a workflow. - Upsert, don't create. Match CRM contacts on email, phone or an external ID. HubSpot, GoHighLevel and most CRMs support some form of upsert or search-then-update.
- Log skipped duplicates. A simple counter per source tells you whether a sender has started retrying more, which is often the first sign your workflow got slower.
That last point matters more than it sounds. When a skipped-duplicate count jumps from a handful a week to dozens a day, the gate is still doing its job, but the log is telling you something upstream slowed down, often an enrichment or LLM API. Without the counter, you'd never notice.
If this is the kind of problem showing up in your lead flow, it's worth checking your CRM lead follow-up automation for the same pattern, since that's where duplicates do the most visible damage.
Get a free automation audit
If your n8n workflows are sending double texts, creating duplicate contacts or charging customers twice, we'll find the cause. Our team reviews your webhook triggers, response settings and side effects, then shows you exactly where an idempotency gate belongs. Get a Free Automation Audit, or see how we build and run n8n workflow automation for agencies and service businesses.
