Skip to content
Flowmanic

Why Your n8n Webhook Automation Creates Duplicate Rows (And How to Actually Stop It)

Shopify, Stripe, and Typeform all warn that webhooks can fire more than once for the same event. Here's why that turns into duplicate CRM rows and double-sent emails, and the check-before-write pattern that actually fixes it in n8n — no database required.

If you've ever opened your CRM sheet and found the same lead logged twice, or had a customer reply "didn't I already get this email?" — the workflow probably isn't broken. It did exactly what it was told, twice, because the webhook that triggered it fired twice.

This isn't rare, and it isn't your automation's fault. It's how webhooks actually work.

Webhooks don't promise "exactly once"

Shopify, Stripe, Typeform, and most other platforms that send webhooks are upfront about this in their own docs: delivery is "at least once," not "exactly once." If their system doesn't get a fast, clean response from you, or has any doubt the first attempt landed, it retries — same event, sent again. From n8n's side, that looks like two separate, completely valid webhook calls carrying identical data.

If your workflow's logic is "webhook comes in → add a row," it has no way to know the second call is a repeat of the first. It just does its job again.

The gotcha that only shows up with real orders

The obvious fix is: check if you've already processed this event before doing anything else. Most people key that check off the order ID, and it works — right up until an order has more than one line item.

Here's the failure mode: the idempotency check is "have I seen this order ID?" A retry for the same order comes in, gets correctly recognized as a duplicate, and gets skipped — except the first delivery only made it through one line item before something interrupted it. The retry that would have processed the second item gets thrown away too, because it shares the same order ID. The order looks fully processed. It isn't.

The fix is keying off the thing that's actually unique — the line-item ID, not the order ID alone. (order_id, line_item_id) together, not order_id by itself. It's an easy detail to miss because a single-item test order never surfaces it — it only shows up the first time a real multi-item order comes through.

How to actually build this in n8n

You don't need a database with a unique constraint to get most of the way there. The same idea works with whatever you're already writing to — Google Sheets, Airtable, a CRM — using a pattern n8n handles natively:

  1. Webhook node receives the event.
  2. Before writing anything, look up whether that event's ID (order ID + line item ID, or whatever your real unique identifier is) already exists in your destination — a Google Sheets "lookup" step, or a search call against your CRM.
  3. IF node branches on what that lookup found: if the ID's already there, stop — respond to the webhook and do nothing else. If it's new, continue to the actual write.
  4. Only then does the row get created, the email get sent, the Slack message get posted.

That's the whole pattern: check before you act, not after. It costs one extra lookup step per run, and it's the difference between a webhook retry being invisible and it silently duplicating everything downstream.

Where to get the identifier from

Pull the ID from the payload itself — the order/line-item ID the source system already assigns — rather than computing your own (like using the item's position in a list). Position-based keys break the moment a retry's payload doesn't list items in exactly the same order, which platforms don't guarantee. An ID sourced from the event itself stays the same no matter how many times that event gets redelivered; one you calculate yourself might not.

The other half: respond fast, respond correctly

Part of why platforms retry aggressively is that they're watching for a fast, successful response to the webhook call itself — not for your automation to finish everything it's going to do. If your workflow does a bunch of slow work (AI calls, multiple API writes) before it gets around to responding, the sender may decide the first attempt failed and retry while your first run is still going. n8n's respondToWebhook node lets you send that acknowledgment early, right after you've confirmed the event isn't a duplicate, and let the rest of the workflow keep running after — so a slow downstream step doesn't look like a failure to the platform that's watching.

Worth the extra step

None of this requires new tools or paid add-ons — just one lookup and one IF node, in a workflow that would otherwise write blind. For anything triggered by a real platform's webhooks (order systems, form submissions, payment events), it's worth building in from the start rather than after the first duplicate lead or double-charged customer shows up. If you're starting from a webhook-triggered template — ours or anyone else's — this is the first thing worth adding once real traffic starts hitting it, not after the first duplicate shows up in your sheet. Our lead capture and order notification templates are a straightforward starting point for exactly that kind of workflow.