This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support — the mistakes are mine, the fixes are tested.
It arrived twice. Both runs said success.
A workflow I ran for months listened for a provider webhook: a payment event, a form submission, a status change. One day a customer got two confirmation emails, and a downstream record was created twice. Both executions were green. Nothing had errored, nothing had retried in n8n — the provider had simply delivered the same event twice.
That is normal webhook behaviour, not a bug. Providers retry deliveries when they do not get a fast 2xx, when a connection drops mid-response, or when their own system replays an event. If your workflow treats "a webhook arrived" as "this happened once", you have no deduplication at all — you have luck, and luck runs out on the busiest day.
A webhook is an untrusted, at-least-once delivery. Design for the second copy, not just the first.
Rule 1: answer fast, then do the work
The first mistake is doing slow work inside the webhook handler. The provider waits for your response; if your workflow takes thirty seconds to call an LLM, write to three systems, and send an email, the provider's timeout fires first and it redelivers — creating the very duplicate you were trying to avoid.
So the webhook node should do as little as possible: validate, record, and return 200. The heavy work moves to a second stage, triggered by the recorded event. In n8n that means the webhook workflow writes a row and stops, and a separate workflow (or a later stage) picks the row up and does the real work.
Fast acknowledgement is not an optimisation. It is the thing that stops the provider from helpfully sending the event again.
Rule 2: dedup on the provider's event id, never on arrival time
The second mistake is deduplicating on the wrong key. Arrival timestamps differ between copies. Execution ids differ. Payloads can differ slightly too — a replayed event may carry a new delivery timestamp or a different attempt count.
The only stable key is the one the provider gives you: the event id in the payload or header. Stripe has one, GitHub has one, most serious providers have one. Store it in a table with a unique constraint, and insert with an atomic "insert if absent" before doing any side effect. The copy that wins the insert proceeds; the copy that loses stops immediately.
If the provider gives you no event id at all, build one from the natural key of the event: the payment id plus the status, the order id plus the event type, the record id plus the change you are reacting to. Never invent a random id per delivery — a random id makes every copy look new, which is exactly the failure you are trying to prevent.
And the claim must be atomic. Check-then-insert in two steps has a race: two copies can both check, both find nothing, and both proceed. A unique constraint plus insert-if-absent is the whole trick; everything else is commentary.
Rule 3: verify the signature before you trust the payload
A webhook endpoint is a public URL. Anyone who finds it can POST anything to it, and a forged event that passes your dedup check will be processed exactly once — which is worse than a duplicate, because it is wrong once, confidently.
So before recording anything, verify the provider's signature: the HMAC over the raw body, compared against the secret you configured, using a constant-time comparison. In n8n, that means keeping the raw body intact until verification — a parsed-and-reserialised body will not match the signature, and a workflow that "fixes" this by skipping verification has removed the only proof the event came from the provider.
An unsigned or badly signed delivery is not a retry candidate. Drop it, log it, and return 200 anyway — returning an error to a forger just invites more attempts, and returning 200 to a legitimate provider that misconfigured its secret is a debugging conversation, not a data problem. Either way, nothing unverified reaches a side effect.
Rule 4: make the downstream work safe to run twice anyway
Dedup at the door is necessary but not sufficient. A workflow can still run twice legitimately: the dedup row is written, the worker crashes before marking it done, and the recovery sweep picks it up again. Or the dedup table itself is reset, and an old event replays.
So the downstream work must also be idempotent on its own terms: upsert by natural key instead of blind insert, check "does this record already exist for this event" before creating it, send with a deterministic message id so a second send is recognisable as a duplicate rather than a new message. The door check stops the common case; downstream idempotency stops the case where the door check itself failed.
This is the same rule as everywhere else in this series: exactly-once is a property of the receiving system, not of the caller. The provider promises at-least-once delivery. Your job is to make at-least-once delivery safe.
The checklist I actually use
- Webhook handler validates, records, and returns 200 fast; slow work runs in a second stage.
- Dedup key is the provider's event id, or a natural key built from the event itself — never a timestamp or a random id.
- Claim is atomic: unique constraint plus insert-if-absent, before any side effect.
- Signature verified on the raw body before anything is recorded or processed.
- Downstream writes are upserts keyed on the event, so a second run is a no-op, not a duplicate.
- Retried deliveries are logged, not alerted on — a duplicate delivery is the provider doing its job, and my workflow handling it quietly is mine doing its job.
It is not glamorous work. No part of it makes webhooks more reliable. But it is the difference between a workflow that processes events and a workflow that trusts the network to deliver each event exactly once — and in production, that difference shows up as a customer getting the same email twice, on the day you least want them to notice.
I'm building AI Automation Lab — production-ready n8n workflow templates. The free AI Hook Generator shows how I structure real workflows; my open-source templates live at github.com/zhp910318/n8n-ai-automation-lab.
Top comments (0)