How can I prevent duplicate workflow executions when a webhook is triggered multiple times?
What is the error message (if any)?
Webhook Set HTTP Request Airtable I’m aware Stripe retries failed requests, but even successful ones sometimes seem to arrive twice. How can I ensure each event is only processed once?
Please share your workflow
(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)
Share the output returned by the last node
I’m using a Webhook node that receives events from Stripe. Occasionally, the same event is delivered more than once, causing my workflow to process duplicate orders
Information on your n8n setup
- n8n version:1.123
- Database (default: SQLite): Postgre sql
- n8n EXECUTIONS_PROCESS setting (default: own, main):
- Running n8n via (Docker, npm, n8n cloud, desktop app):
- Operating system:
Hi @Fatoki_Alfred
Every Stripe event contains a unique id (e.g., evt_1Nabc...). To ensure each event is processed only once, you should track these IDs in a dedicated “processed events” table in your Postgres database.
Hi @Fatoki_Alfred
n8n has a built-in Remove Duplicates node that does this with no custom table or query. Drop it right after the Webhook node and set Operation to “Remove Items Processed in Previous Executions”, Keep Items Where to “Value Is New”, and Value to Dedupe On to the Stripe event id:
{{ $json.body.id }}
n8n keeps the seen-id history in its own Postgres database, so it persists across restarts, and any repeat delivery is dropped before it reaches your order steps.
Complementing the answers above: the point that usually gets missed is the race condition. If Stripe delivers the same evt_id in parallel, two workflows can pass the deduplication check BEFORE either one saves the id — and both proceed. The Remove Duplicates node solves the serial case, but it’s not atomic under concurrency.
To truly protect yourself, do the deduplication in the database itself: create the column with a UNIQUE constraint (e.g., stripe_event_id TEXT UNIQUE) and, right after the Webhook, a Postgres node with INSERT … ON CONFLICT (stripe_event_id) DO NOTHING RETURNING id. If RETURNING comes back empty, it’s a duplicate redelivery → stop the flow there (use an IF checking whether a row was returned). The database guarantees atomicity, so even two simultaneous deliveries: only one wins the INSERT.
Two extra things that greatly reduce the problem at the source: (1) respond 200 to Stripe as quickly as possible (use the Webhook in Respond Immediately mode or a Respond to Webhook node at the start) — timeouts make Stripe retry and that’s where many ‘duplicates’ are born; and (2) process the order only after the control INSERT succeeds. This way the event id is your real idempotency key, and the order logic never runs twice.
Stripe webhook retries are expected, so duplicate deliveries aren’t an error. Rather than assuming each webhook is unique, make the workflow idempotent.
A simple pattern is to check the incoming “event.id” against your Data Store or database before doing any processing. If it already exists, exit the workflow. If not, store the ID and continue.
An IF node before your business logic is usually enough, and enforcing “event.id” as a unique field in your database adds another safeguard.
Hi @Fatoki_Alfred
Stripe intentionally retries webhook deliveries, so duplicate events are expected. The recommended approach is to make your workflow idempotent instead of assuming each webhook is unique.
The simplest solution is to store the Stripe event.id before processing. At the start of the workflow:
- Extract event.id.
- Query your database (or Data Store) for that ID.
If it already exists, stop the workflow.
Otherwise save the ID and continue processing.
Using an IF node before your business logic is usually enough.
If you’re writing to Airtable or another database, consider making event.id a unique field so duplicates are rejected automatically.
Treat Stripe webhooks as at-least-once delivery and make the workflow idempotent. Use the Stripe event ID as the idempotency key. Near the start of the workflow, verify the webhook signature and try to insert that event ID into a PostgreSQL table with a unique constraint. Continue only if the insert succeeds. If the ID already exists, return a successful response and stop without creating the order again.
A simple table can contain event_id as the primary key plus status, received_at and completed_at. In the Postgres node use an insert with ON CONFLICT DO NOTHING and return whether a row was inserted. Send that result to an IF node. The true branch processes the order and the false branch exits. The database constraint matters because two copies can arrive close enough together that a separate lookup-then-insert check lets both through.
Mark the record as processing when claimed and completed only after the order succeeds. Decide how failed records should be retried so a temporary error does not permanently suppress the event. Also pass the Stripe event ID to downstream systems as their idempotency key where supported. Do not deduplicate by customer, amount or timestamp because separate legitimate payments can share those values.
Create an idempotency key from a stable event ID and store it before the expensive nodes run. If the same key arrives again return early and set an expiry that matches how long the sender may retry.