Your workflow ran twice. Now you have two invoices.
An API timeout, an automatic retry, a duplicate booking. Most automation teams do not build for this until something breaks. Here is the concept and the fix.
Your workflow ran twice. Now you have two invoices.
API timeouts happen. Retry logic is built into almost every automation platform. A timeout followed by an automatic retry is one of the most frequent sources of duplicate data in production automation systems.
The problem is not the retry. Retries are useful. The problem is that most workflows are not built to handle them.
The concept: idempotency
An operation is idempotent if running it multiple times produces the same result as running it once. The math notation is f(f(x)) = f(x), but the practical definition is simpler: if a workflow processes the same event twice, the outcome should be the same as if it ran once.
Most workflows are not idempotent. They run once by design, and when they run twice, they do the thing twice.
The three ways it happens
Timeouts and automatic retry
Zapier, n8n, and Make all have built-in retry behavior. n8n retries webhooks up to three times by default. If a webhook endpoint does not respond fast enough because a downstream API was slow, the platform fires again. If the workflow had already written data before the timeout, it writes again.
At-least-once delivery from external APIs
Stripe, Shopify, and HubSpot all document this: they guarantee "at least once delivery," not "exactly once." Stripe's webhook documentation says explicitly that the same event can arrive multiple times and that endpoints should handle duplicates. If an automation creates an order when a `payment_intent.succeeded` event arrives, and Stripe sends it twice, the workflow creates two orders. That is not a Stripe bug.
Manual replays after failure
This one happens constantly. A workflow fails at step 4. Someone clicks replay. The workflow re-runs from the beginning, but steps 1 through 3 may have already written data. You end up with a partial duplicate: some things happened twice, some happened once, and figuring out which is which takes time.
Why it stays hidden
Duplicate data from non-idempotent workflows rarely causes an immediate crash. That is what makes it hard to catch.
A duplicate invoice booking shows up at month-end close. A duplicate CRM contact sits in the database for weeks until a salesperson notices they are calling the same person twice. A duplicate order confirmation gets dismissed by the customer as a system glitch.
By the time someone investigates, the original event is gone from the logs. You see the bad state but not the moment it was created.
Three ways to fix it
Idempotency keys
Every incoming event gets a unique ID. Before the workflow writes anything, it checks: have I already processed this ID? If yes, stop. If no, process and record the ID as done.
Stripe puts a unique `id` field on every webhook event. So does Shopify. Store processed IDs in a database table or a Redis key with a reasonable TTL. Check before every write.
```javascript if (await db.exists('processed_events', webhookId)) { return { status: 'already_processed' }; } await processWebhook(data); await db.insert('processed_events', webhookId, { processedAt: now() }); ```
Upsert instead of insert
Instead of a plain INSERT, use INSERT ... ON CONFLICT DO UPDATE. If the same record arrives twice, it gets updated, not duplicated. This works well for reference data: contacts, products, configuration entries. For transaction records like invoices and orders, upsert alone is not enough. A duplicate transaction is a business error regardless of whether the data matches.
Business-level deduplication
Instead of relying on technical IDs: check whether a semantically identical record already exists. "An order for customer Y with reference X was already created today" as a query before writing. More work, but more robust when technical event IDs vary across retry attempts.
What actually needs to be idempotent
Not everything. A workflow that sends an internal Slack notification when a support ticket opens is fine if it fires twice.
But: anything that books money, creates a contract, sends an email to an external recipient, or writes to a CRM should be idempotent.
The question I ask for every data-writing workflow: what happens if this step runs twice? If the answer is "the same as once": fine. If the answer is "I am not sure" or "probably two of something": that is the thing to fix.
How to audit what you have
Start with three questions:
Which of your workflows write to production systems? Of those, which are triggered by external sources with retry logic or at-least-once delivery semantics? And for those, is there a deduplication check in place?
Any "no" on the third question is a candidate for work. At low volume these failures stay hidden. As volume grows they surface with increasing regularity, and cleaning up the data after the fact costs more than preventing it.
The free Automations Check covers this as part of its walkthrough in about 30 minutes.