The Idempotency Checklist Every Customer-Facing Automation Needs
A pre-launch checklist that stops your automation from sending duplicate emails, double-charging cards, or firing the same Slack alert three times when a workflow retries.
On this page
Every customer-facing automation will retry. The question is whether your workflow handles that gracefully or whether your customer gets billed twice, emailed four times, and shipped two of the same order.
Most automation builders learn this the hard way. A webhook times out, the upstream service retries, and suddenly your CRM has three duplicate contacts and your Stripe dashboard shows two charges. The fix isn't "make retries stop" - retries are a feature, not a bug. The fix is making every side effect in your workflow safe to run more than once. That's idempotency.
Here's the checklist I run through before any customer-facing automation goes live in n8n, Make, Zapier, or custom code.
1. Assume at-least-once delivery, always#
Webhooks, queues, and HTTP retries all guarantee at-least-once delivery. Not exactly-once. The Stripe webhook docs say it plainly: the same event can arrive twice. So does AWS SQS. So does Shopify. So does your own n8n workflow when a downstream node fails and the execution retries from the last successful checkpoint.
Before writing a single node, ask: if this workflow runs twice with the exact same input, what breaks? If the answer involves money, email, SMS, inventory, or a customer-visible record, you need explicit idempotency controls. If the answer is "nothing, it just overwrites the same row," you're probably fine - but verify, don't assume.
2. Pick a real deduplication key#
Every incoming event needs a stable, unique identifier you can use to recognize a repeat. Bad choices: timestamps, auto-incremented IDs you generate yourself, anything derived from now(). Good choices: the event ID from the source system (evtabc123 from Stripe, the Shopify order ID, the message ID from your queue), or a hash of the payload's stable fields.
For workflows triggered by forms or internal events where no natural ID exists, generate a UUID at the entry point and pass it through every node. Store it. Check it. Never regenerate it on retry.
Write this key to a dedupe store - Redis with a TTL, a Postgres table with a unique index, an Airtable record, even a Google Sheet for low-volume cases. The first node after your trigger should check: have I seen this key in the last N hours? If yes, exit cleanly.
3. Upsert instead of insert#
Any database write in a customer-facing automation should be an upsert, not a blind insert. In Postgres that's INSERT ... ON CONFLICT (key) DO UPDATE. In Airtable, search by your dedupe field first and update if found. In HubSpot and Salesforce, use the external ID field so the platform handles the merge for you.
The trap is that most no-code integrations default to "Create." You have to explicitly choose "Create or Update" and configure which field is the match key. If that field is empty on the incoming payload, you'll get duplicates anyway - so validate the match key is present before the write node runs, and route missing-key records to a manual review queue.
4. Make external side effects conditional#
For anything you cannot undo - sending an email, charging a card, posting to Slack, shipping inventory, calling a webhook - guard the action with a check against your dedupe store or against the target system itself.
For email: before calling SendGrid or Postmark, query your log table for (customerid, emailtemplate, datebucket). If a row exists, skip. After sending, write the row in the same transaction or as the very next step.
For payments: Stripe accepts an Idempotency-Key header on charge creation. Pass your event's dedupe key here. Stripe will return the original charge instead of creating a new one. Use this. Always.
For Slack and other notifications: most teams accept some duplication on alerts, but customer-facing messages (order confirmations, appointment reminders) need the same dedupe-log pattern as email.
5. Mind the alwaysOutputData and continueOnFail traps#
In n8n specifically, two settings will quietly break idempotency if you're not careful. Always Output Data makes a node return an empty item even when it found nothing - which can cause downstream nodes to run with stale or empty input and produce ghost records. Continue On Fail lets execution proceed past errors, which means a failed write can be followed by a successful email saying "your order is confirmed" when nothing was actually saved.
Rule of thumb: turn these on deliberately, not by default. If a node has Continue On Fail enabled, the very next node should check the previous node's output for an error and branch accordingly. Don't let the happy path run on a broken state.
Make and Zapier have analogous traps - Make's error handlers and Zapier's auto-replay can both re-run partial workflows. Read the retry semantics of whatever platform you're on, in writing, before launch.
6. Use transactions or compensating actions#
When a workflow does two things that must both succeed or both fail (charge the card AND create the order), you have two options. Option one: wrap both in a database transaction at the application layer. Most no-code tools can't do this, so you fall back to option two: compensating actions.
A compensating action means if step two fails after step one succeeded, step three reverses step one. Charge succeeded but order creation failed? Refund the charge and log the incident. Email sent but CRM update failed? Add a task for a human to reconcile. The point is to never leave the customer in a state where they paid but have no record, or have a record but weren't charged.
7. Build a kill switch and a replay tool#
Before go-live, you need two operational tools. First, a kill switch - a flag in a config table or environment variable that your trigger node checks before doing anything. When something goes wrong at 2am, you flip the flag and the workflow stops accepting new work without you having to disable the whole automation.
Second, a replay tool. If a batch of events failed because an API was down, you need a way to re-run them by dedupe key, knowing your idempotency controls will prevent doubles. This is usually a simple admin workflow that pulls failed records from a logs table and re-injects them into the main flow. If you can't safely replay, you can't safely operate.
8. Test the failure modes before launch#
The checklist isn't done until you've actually tried to break it. Run the same payload through twice and confirm no duplicates. Kill the workflow mid-execution and let it retry - confirm the customer state is correct. Point a load test at the webhook endpoint and watch for race conditions where two simultaneous executions both pass the dedupe check before either writes the lock.
That last one is the subtle bug that kills production systems. The fix is either a database-level unique constraint (so the second insert fails loudly) or a proper distributed lock (Redis SETNX with a TTL). Pick one and verify it works under concurrent load, not just sequential testing.
The pre-launch sign-off#
Before any customer-facing automation handles real traffic, every item on this list should have a clear answer: what's the dedupe key, where is it stored, what's the TTL, which writes are upserts, which external calls are guarded, what's the kill switch, what's the replay path, and what did we observe under the failure tests we actually ran.
If you can't answer those in one page, you're not ready to launch - you're ready to apologize to customers.
We build these controls into every workflow we ship, and we audit existing automations for the same gaps. If you want a second set of eyes on something before it goes live, start with our process.
Need help implementing this?
We build these systems for small businesses and hand you the keys. Book a free discovery call — no sales pressure.
Book a Discovery CallFrequently asked questions
What does idempotent mean in the context of an automation?
An automation is idempotent when running it twice with the same input produces the same result as running it once. No duplicate emails, no double charges, no extra database rows - the second run is a safe no-op.
How do I prevent duplicate emails when my workflow retries?
Before sending, check a log table keyed by recipient, template, and time window. If a row exists, skip the send. Write the log row immediately after the send succeeds so the next retry finds it.
What is a good deduplication key for webhook automations?
Use the event ID provided by the source system - Stripe's evt_ ID, Shopify's order ID, or a queue message ID. If no natural ID exists, generate a UUID at the trigger and pass it through every step. Avoid timestamps or self-generated sequential IDs.
Does n8n handle idempotency automatically?
No. n8n retries failed executions and can re-run nodes, but it does not deduplicate side effects like emails or API calls. You have to add dedupe checks, upsert logic, and idempotency keys yourself. Watch the Always Output Data and Continue On Fail settings - both can cause silent duplication.
How does Stripe's idempotency key work?
Stripe accepts an Idempotency-Key header on POST requests. If you send the same key twice within 24 hours, Stripe returns the original response instead of creating a second charge or customer. Use your event's dedupe ID as the value.
What is at-least-once delivery and why does it matter?
At-least-once delivery means a message or webhook is guaranteed to arrive, but it may arrive more than once. Most webhook systems and queues use this model. Your automation must assume duplicates will happen and handle them safely rather than trying to prevent them upstream.