
How to Prevent Duplicate Webhooks in n8n (Without Losing Real Events)
A webhook reaches your n8n workflow.
The workflow creates a contact in your CRM, assigns an owner, and notifies somebody in Slack.
Everything works.
Two minutes later, the same webhook arrives again.
Your CRM now has two contacts. Or the sales team gets two notifications. Or a customer receives the same welcome email twice.
This is one of those automation problems that does not look difficult until you ask the next question:
If I block duplicate events, how do I make sure I do not accidentally block a legitimate retry after something failed?
That question separates a quick automation demo from a system you can trust.
The good news is that n8n has a built-in Remove Duplicates node that can remember values from previous workflow executions. For ordinary, low-risk webhook traffic, this is often a useful starting point. The harder part is choosing the right event identity, understanding what n8n stores, deciding where the node belongs, and knowing when its history is not enough.
This guide covers both sides: the simple setup and the production limitations. It also points to a real open-source n8n Webhook Replay Shield workflow I published so you can inspect the JSON instead of relying only on a description.
Quick answer: Use one incoming webhook, extract a stable event ID, place Remove Duplicates before your side-effect nodes, and allow only first-seen events through. For important transactions or recovery-sensitive systems, add a durable idempotency record and make the destination operation itself retry-safe.
Why do webhooks arrive more than once?
A webhook is a message sent by one service to another when something happens.
A payment succeeds. A lead submits a form. A deal moves stage. A customer completes onboarding.
The sending service makes an HTTP request to your workflow. Ideally, your workflow accepts that request, does its work, and returns a response.
But an HTTP request and a business operation are not the same thing.
Imagine this sequence:
- A CRM sends a new-lead event.
- n8n receives it.
- n8n successfully creates a task and notifies the team.
- The HTTP connection times out before the sender sees the response.
- The sender assumes the delivery may have failed and sends it again.
From the sender's perspective, the result is uncertain. From the business's perspective, the work may already be complete.
Retries exist to avoid losing events. Duplicate prevention exists to avoid doing the work twice. You usually need both.
Not every webhook provider retries in the same way. Stripe documents automatic retries for unsuccessfully delivered events. GitHub's webhook documentation says GitHub does not automatically redeliver failed deliveries; those can be redelivered manually or through a recovery script. That difference matters whenever you design a generic integration. Do not assume one provider's behavior applies to every webhook.
Sources: Stripe event delivery behaviors, GitHub failed webhook deliveries.
What actually counts as a duplicate?
This sounds obvious, but it is the most important design decision.
There are at least three different kinds of duplication.
1. The same event was delivered twice
The source system produces one event:
{
"event_id": "evt_009821",
"source": "crm",
"type": "lead.created",
"lead_id": "lead_407"
}
A network retry sends that same event ID again.
This is delivery duplication. A history of processed event IDs is designed to identify it.
2. Two different events refer to the same business record
Now imagine:
{
"event_id": "evt_009822",
"source": "crm",
"type": "lead.updated",
"lead_id": "lead_407"
}
The event ID changed, but lead_id stayed the same.
This is not automatically a duplicate. It could be a legitimate update to an existing record. If you deduplicate only by the lead ID, you may throw away important changes.
That is why event deduplication and customer deduplication are different problems.
- Event deduplication asks: Have I already processed this particular event?
- Customer deduplication asks: Does this customer already exist?
- Business-operation idempotency asks: Would doing this operation again create another effect?
A reliable CRM workflow may need all three.
3. Different events represent the same underlying change
Some providers can emit more than one event object that describes essentially the same underlying operation.
Stripe's documentation explicitly distinguishes repeated delivery of the same Event object from cases where two distinct Event objects are generated. It recommends using the relevant data.object ID together with event.type when detecting the latter kind of duplication.
That is not a rule to blindly reuse for every platform. It is an example of why you must understand the sender's event model before deciding which key means "already processed."
Source: Stripe — Handle duplicate events.
How n8n remembers an event ID
n8n's Remove Duplicates node has an operation named Remove Items Processed in Previous Executions.
In that mode, n8n compares a selected value from the incoming item against a history of previously processed values. With the Value Is New rule, it keeps items whose value has not yet been seen and filters out the ones that have.
That history belongs to n8n's internal persisted deduplication state. You do not need to send the event to a second webhook or create your own spreadsheet to use this starter pattern.
The core configuration is:
| Setting | Suggested starting value | Why |
| --- | --- | --- |
| Operation | Remove Items Processed in Previous Executions | Compare events between runs, not just inside one batch |
| Keep Items Where | Value Is New | Only first-seen keys continue |
| Value to Dedupe On | {{ $json.eventKey }} | Compare a stable, normalized key |
| Scope | Node | Avoid accidentally sharing state with a different check |
| History Size | 10,000 initially | Bound stored IDs; tune to your traffic and retry horizon |
History Size is a count, not an expiration timer. A value such as 10,000 does not mean an event automatically expires after ten minutes, ten hours, or ten days.
Nor is the history a permanent, unlimited database of every event forever. You need to understand your n8n version's history behavior, how it fills, what happens when the history is cleared or reset, and whether your retention choice covers the sender's retry window.
If you expect hundreds of thousands of events or need to inspect the state externally, move beyond the built-in history.
References: n8n Remove Duplicates documentation, worked examples.
Do you need a separate webhook for duplicate checking?
No. Keep your existing webhook.
For example, a simple lead workflow might originally look like:
Webhook → Normalize lead → Create CRM contact → Notify sales
You can change it to:
Webhook → Verify sender → Normalize event ID → Remove Duplicates → Create/update CRM contact → Notify sales
Nothing needs to be sent to another endpoint. The Remove Duplicates node simply becomes another step in the existing workflow.
When a first-seen event arrives, it flows forward.
When the same event key arrives again, it is removed from the output items. Your CRM and Slack steps receive nothing from that event.
That last detail matters. In the n8n Remove Duplicates v2 implementation, Kept and Discarded are results you can inspect; they are not two independent outgoing connections that you can wire to two separate Code nodes.
I originally made that mistake in a public workflow export. After spotting the issue, I corrected the JSON, removed the unreachable duplicate-response branch, and added a test to prevent that invalid connection from returning. The current Replay Shield source has one supported outgoing path from Remove Duplicates.
It is a useful reminder: a JSON file can pass a basic structure check and still need actual n8n node-level validation.
Which ID should you use?
The best event key is normally a stable identifier supplied by the event producer. It should be reused when the same event is redelivered.
A practical rule is:
event key = source + event ID
For example:
| Source | Event ID | Resulting key |
| --- | --- | --- |
| CRM | evt_881 | crm:evt_881 |
| Billing | evt_881 | billing:evt_881 |
| CRM | evt_882 | crm:evt_882 |
The same raw identifier from two unrelated systems no longer collides.
Do not automatically use the contact email, the current timestamp, or a random number generated inside n8n as the key.
- Email: The same person might legitimately submit another form or create another inquiry.
- Customer ID: One person can generate many valid events.
- Current timestamp: It changes on each delivery, so a retry looks new.
- Random UUID created during processing: It also changes every time unless the sender itself persists and reuses that UUID.
For webhook providers with a standard delivery ID, use the provider's documented identity. GitHub, for example, documents the X-GitHub-Delivery header as an identifier that remains the same when a delivery is redelivered.
For a custom lead form, the form-submission system should create and persist an event ID once per submission. When the same submission is retried, it must send that ID again.
Reference: GitHub webhook best practices.
A simple n8n setup, step by step
Here is how I would approach a normal CRM webhook.
Step 1: Look at the real webhook payload
Before adding deduplication, capture one fictional or sanitized sample event.
Inspect both the JSON body and relevant headers.
Find:
- the stable event or delivery identifier
- the system that sent it
- the type of event
- the business record identifier
- the timestamp or version, when supplied
- any provider signature or authentication headers
Don't assume id always means event ID. It might be a contact ID, invoice ID, workflow execution ID, or some other value.
Step 2: Create one normalized event key
Use an Edit Fields node when the payload is simple. A Code node is reasonable when you receive several shapes.
For example, suppose you already have a trusted, authenticated event ID and source:
const items = $input.all();
return items.map((item) => {
const body = item.json.body ?? item.json;
const source = String(body.source ?? "").trim().toLowerCase();
const eventId = String(body.event_id ?? body.eventId ?? "").trim();
if (!source || !eventId) {
throw new Error("Missing stable source or event ID");
}
return {
json: {
...item.json,
eventKey: source + ":" + eventId,
},
...(item.binary ? { binary: item.binary } : {}),
};
});
This is a minimal illustrative Code node, not a full request validator. In a real workflow you should validate the sender-specific format, limit input sizes, and reject unauthenticated events before accepting an ID as genuine.
Notice the line that retains ...item.json. That matters if the rest of your workflow still needs the customer's name, lead ID, or other fields.
A strict demo that strips those fields can be safer to publish publicly, but you should not drop the data required for your actual CRM integration.
Step 3: Add Remove Duplicates
Place Remove Duplicates after the validation/mapping step, but before the first action that could cause an external effect.
Choose:
- Remove Items Processed in Previous Executions
- Value Is New
- Value to dedupe:
{{ $json.eventKey }} - Scope: Node
- A history size appropriate to your workload
Now connect the node's single main output to the first CRM operation.
Step 4: Test first delivery and retry separately
Use a new event key and submit it once.
You should see one item flow to the next node.
Then send that exact same event a second time in another workflow execution.
This time, the key should be recognized as already seen and the event should not continue to the CRM node.
Then change the event ID and send the request again. It should be treated as a new event.
Test the published workflow too. The exact history behavior between manual/test and production execution modes can vary with instance and workflow state, so a successful editor test is not proof that your live endpoint is fully covered.
Step 5: Check what HTTP response the sender receives
This detail is easy to overlook.
If the duplicate is discarded before the workflow reaches a Respond to Webhook node, n8n's documented behavior is to return its standard HTTP 200 response because the workflow finished without executing a response node.
That does not mean a special duplicate JSON body was generated.
If a provider or your own calling application expects a particular receipt structure, plan for that separately. The default "drop the duplicate" path is fine for some senders, but not every integration contract.
Reference: n8n Respond to Webhook — workflow behavior.
What the workflow should look like in three real scenarios
Let's use the same event, crm:evt_881, across several runs.
Scenario A: The event is genuinely new
10:00:00 — The sender emits crm:evt_881.
10:00:01 — The n8n workflow validates it.
10:00:01 — Remove Duplicates does not recognize the key and records it in its history.
10:00:02 — The event continues to the CRM node.
10:00:03 — The contact is created or updated.
10:00:04 — Your workflow sends an acknowledgement.
This is the straightforward case.
Scenario B: The sender retries after a timeout
10:00:00 — The sender emits crm:evt_881.
10:00:03 — Your CRM action succeeds, but the HTTP response is lost.
10:02:00 — The sender resends crm:evt_881.
10:02:01 — The duplicate check recognizes the key.
10:02:01 — The event does not proceed to the CRM step again.
This is where the basic history filter helps.
Scenario C: The CRM action failed
10:00:00 — The sender emits crm:evt_881.
10:00:01 — Remove Duplicates records the key as seen.
10:00:02 — The CRM rejects the write because its API is unavailable.
10:02:00 — The sender retries crm:evt_881.
10:02:01 — The duplicate check rejects it as already seen.
Now you have a problem: the workflow prevented the retry of a job that never completed.
The event was received, but the intended work may not have happened.
That is the biggest limitation of placing a simple "remember this key" step in front of external side effects.
Deduplication is not the same as idempotency
These words are related, but they are not interchangeable.
Deduplication detects that an event or value has already been seen.
Idempotency means that repeating an operation does not change the final business outcome beyond the first successful application of that operation.
For example:
- "Create a new contact" on every retry is usually not idempotent.
- "Update the contact identified by this stable external ID" can be designed to be idempotent.
- "Charge this card again" is not automatically idempotent.
- "Process this payment request with the provider's documented idempotency key" can provide stronger protections, according to that provider's contract.
A history filter can reduce repeated calls. It cannot, by itself, prove that the first call completed.
That is why I treat the built-in Remove Duplicates node as a useful gate, not as an exactly-once processing guarantee.
When the built-in Remove Duplicates node is enough
A small, simple replay filter can be reasonable when all of these are true:
- You have a stable event ID that the sender reuses.
- Duplicate processing would be inconvenient rather than financially dangerous.
- Losing a retry after a downstream failure would be tolerable or recoverable manually.
- The volume is small enough for the built-in history.
- You have tested duplicate behavior across separate, published executions.
- You understand the sender's retry horizon and your n8n history settings.
For example, avoiding repeated informational Slack alerts for the same noncritical update can be a good fit.
This is also a good starter pattern for internal tools where somebody reviews failed runs and can replay work safely.
The critical word is recoverable.
If an event is dropped or an integration goes down, can your team detect it, reconcile the missing work, and put things back into the right state?
If not, the workflow deserves a stronger architecture.
When you should use a database instead
I would move beyond the basic node when the action involves:
- payments, invoicing, refunds, or credits
- account access, provisioning, or deprovisioning
- inventory reservations
- legally significant communications
- a large volume of concurrent events
- delivery windows longer than the available history
- operations that must be retried until confirmed successful
- audit requirements that need a queryable record of exactly what happened
For these cases, the duplicate record should say more than "I saw this ID."
It should keep track of what happened to the work.
A minimal model might contain:
| Field | Meaning |
| --- | --- |
| source | The sender or integration |
| event_id | The stable event identity |
| status | pending, succeeded, or failed |
| attempts | How many times processing was attempted |
| created_at | When the event was first accepted |
| updated_at | When the state last changed |
| receipt | A safe identifier for the completed external action |
A unique database constraint on (source, event_id) lets you atomically reserve one event key. Two concurrent workers cannot both insert the same unique key successfully.
That is much stronger than doing a separate "search for the event" step and hoping nobody writes it between your search and your insert.
What a durable flow looks like
A stronger design is:
Authenticated webhook → Validate event → Atomic event reservation → Durable work queue → Retry-safe destination operation → Mark succeeded
When the same event arrives again:
- If it already succeeded, return the prior receipt or acknowledge it as already complete.
- If it is pending, do not launch a second competing operation without a clear lease/recovery rule.
- If it failed, decide whether and how to retry it safely.
The tricky case is a crash between the external operation succeeding and the database recording that success.
For that reason, a durable ledger should usually be paired with a destination-side unique external ID, upsert, or supported idempotency key. If the destination API cannot deduplicate the operation, there may still be an unavoidable failure window.
Exactly-once effects are a property of the entire system, not of one n8n node.
A PostgreSQL example: atomic reservation
For readers building a heavier integration, here is a simplified PostgreSQL pattern.
First create a table with a unique event identity:
CREATE TABLE webhook_events (
source TEXT NOT NULL,
event_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'succeeded', 'failed')),
attempts INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (source, event_id)
);
Then attempt to reserve the event:
INSERT INTO webhook_events (source, event_id)
VALUES ($1, $2)
ON CONFLICT (source, event_id) DO NOTHING
RETURNING source, event_id, status;
If a row is returned, this insert created the reservation. If no row is returned, a matching key already exists and your logic should inspect its state.
Use parameterized SQL; do not concatenate customer input into SQL strings.
This is only the reservation step, not a complete reliable event processor. You still need a lease or queue policy for stuck pending work, safe retries, destination-side idempotency, and recovery if a worker crashes.
The important architectural idea is that the unique constraint makes identity reservation atomic. A Code node checking a JavaScript array cannot provide the same guarantee across workers.
Reference: PostgreSQL INSERT / ON CONFLICT.
A practical CRM example: create vs. upsert
Imagine your automation receives a new website lead.
A naive workflow might be:
Form webhook → Create contact → Send message → Create task
A more dependable design is:
Webhook → Authenticate → Normalize event → Dedupe → Search or upsert contact by stable CRM identity → Assign owner → Create task idempotently → Notify
There are two different identities to preserve:
- The webhook event ID answers whether this specific submission was received before.
- The CRM contact ID or external customer key answers which contact should be updated.
If you use the event ID as the CRM contact ID, the same person could be created again on their next genuine submission.
If you use the contact ID as the event ID, you might discard all future changes to that person.
A trustworthy lead-intake system models both.
For more on protecting ownership and existing customer relationships, see my guide to lead routing rules.
Don't confuse retries with error recovery
It is tempting to solve every integration problem with "Retry On Fail" and a long wait.
But retry logic needs to distinguish two failure classes.
Retryable: A temporary network failure, a transient server error, or a rate limit with a documented retry policy.
Not automatically retryable: An invalid payload, rejected permission, missing mandatory field, or an operation that may have succeeded but returned an ambiguous result.
Even apparently retryable errors can be dangerous for actions that are not idempotent.
Suppose n8n calls an API to create an invoice. The API completes the request, but n8n times out before receiving the response. Retrying "create invoice" can create a second invoice unless the destination has a stable idempotency key or a unique business reference.
For failure monitoring, use n8n's error workflows and execution history as part of an operating process. But don't mistake "we can retry the execution" for "the destination guarantees we cannot repeat an effect."
Useful reference: n8n execution history and retrying failed workflows.
Authentication comes before trust
A stable event ID is not proof that the event is genuine.
If an attacker can send arbitrary requests to a public webhook, they could invent IDs, poison the duplicate history, or trigger actions using fabricated customer data.
The workflow should verify the source before trusting the event identity.
Depending on the provider, that may mean:
- a documented HMAC signature over the raw request body
- a signed delivery header
- a secret or header-based authentication mechanism where appropriate
- HTTPS and restricted access
- replay protections tied to the provider's real event format
For signed webhook providers, a payload can fail verification if you alter the raw body before checking its signature. Follow the provider's exact signing contract. Do not replace provider verification with a made-up "shared secret" field inside an untrusted JSON body.
GitHub recommends a webhook secret and uses a signature header. Stripe provides its own signature-verification process and explicitly warns against taking unverified webhook requests as genuine.
Sources: GitHub validating webhook deliveries, Stripe webhook security.
Where does n8n store the IDs, and for how long?
In the built-in Remove Duplicates operation, n8n keeps a history of previous deduplication values. The Scope option determines whether that history is specific to the node or shared at the workflow level.
This is distinct from storing your own row in a PostgreSQL table or maintaining a full incident log.
Three separate things are easy to mix up:
- Deduplication history: the values n8n uses to decide whether a key has been seen.
- Execution history: saved inputs, outputs, and run details used for debugging.
- Business state: whether the CRM contact was created, payment was applied, or customer received access.
The first does not automatically establish the third.
And reducing data stored in the first does not mean personal details from incoming webhook requests never appear in the second. n8n has workflow settings for saving execution data and, depending on edition and version, additional redaction capabilities.
If the payload contains contact information or sensitive fields, review execution-data settings and access rights before using live customer data.
Source: n8n execution-data management.
How to choose a sensible history size
There is no magic value that works for every workflow.
You need to consider:
- peak events per hour or day
- the longest plausible provider retry or manual-redelivery window
- how many distinct event keys arrive during that time
- whether history is shared between different nodes
- the n8n version's behavior when history fills
- how you will recover after history resets or workflow changes
For a simple internal integration, you might start with a bounded history and observe it.
For a payment integration where events can be redelivered much later, a fixed-length recent history might be far too small. A durable store with a deliberate retention period and recovery path is the safer choice.
Remember: 10,000 keys represents 10,000 keys. It does not represent a particular number of days.
A testing checklist I would run before trusting it
A normal happy-path test is not enough.
At minimum, run these scenarios against your test environment:
| Test | What should happen | | --- | --- | | First delivery with a valid event ID | Business action runs once | | Same ID delivered immediately again | Second action suppressed | | Same ID delivered in a later execution | Still identified as seen | | New ID for the same customer | Event is not incorrectly discarded | | Same event ID from a different source | Separate source-scoped key | | Event without an ID | Rejected or quarantined, not processed blindly | | Invalid signature | Rejected before dedupe or business actions | | CRM API fails after key is recorded | Failure is visible; recoverable procedure exists | | Two identical events arrive concurrently | Destination has a uniqueness/idempotency guard | | Older event arrives after a newer update | State does not move backward blindly | | Workflow redeployed or restarted | History and recovery assumptions verified | | Duplicate webhook reaches no response node | Sender receives the expected protocol response |
The failed-after-key-recorded test is particularly valuable. It forces you to prove that your reliability plan handles more than duplicate suppression.
For provider webhooks, also test signature verification using the sender's actual documented method.
Common mistakes that make duplicate protection unreliable
Checking the full JSON instead of a stable identity
Two deliveries of the same event can contain changing metadata, attempt counters, delivery timestamps, or field ordering.
Comparing the whole payload may make the same underlying event appear different.
Prefer a sender-stable identity unless the provider's documented model requires a different composite key.
Generating the ID inside the receiving workflow
If n8n creates a new random ID when each webhook arrives, every retry looks unique. The ID needs to originate with the event or be deterministically derived from a provider-supported stable reference.
Treating every contact update as a duplicate
The same person can legitimately submit multiple forms, book another appointment, or request a second service. Customer identity and event identity should not be confused.
Clearing history during debugging without considering production
Once a history is reset, previously seen events may be accepted again. Know which node scope and environment your tests affect.
Wiring a second output that the node does not expose
For n8n's Remove Duplicates v2, discarded items do not create a second normal canvas route to a custom duplicate-response Code node. Inspect the live node's connections, not just a hand-written JSON diagram.
Responding 200 before work is safely recorded
An HTTP success response often tells the sender to stop retrying. If you acknowledge an event before it is durably recorded, and then lose the work, the provider may never send it again.
For heavier integrations, prefer accepting a verified event into a durable queue or ledger first, then acknowledging receipt.
Assuming 200 means the business action succeeded
A transport acknowledgement can mean "I accepted the event," "I recognized a duplicate," or "the platform returned its default fallback."
It does not necessarily mean the invoice was created or the contact was updated.
What I actually published in my Automation Lab
To make the starter pattern inspectable, I published a free Webhook Replay Shield for n8n.
The repository includes:
- a public
workflow.jsonfile that you can import - a sample fictional webhook request
- expected first-seen and invalid-request results
- a strict event-envelope validator
- a Remove Duplicates node configured with source-scoped keys
- tests for the node graph, Code-node behavior and reachable connections
- documentation about authentication, history limits and production trade-offs
The example is intentionally inactive and credential-free. It does not secretly call a CRM, send messages, charge customers, or promise to be a complete transaction ledger.
It also deliberately removes unneeded customer fields after validation. If you copy the deduplication step into an existing lead workflow, preserve the fields that your later nodes require.
The sample uses this basic structure:
Webhook → Validate event → Remove Duplicates → Respond
An invalid event takes a separate validation path.
A duplicate has no downstream item and gets n8n's standard HTTP 200 fallback rather than a custom duplicate-response JSON payload.
This is useful as a teaching artifact and starter template. It is not a claim that I ran an independent benchmark showing a particular reduction in CRM duplicates, nor a claim that the demo guarantees exactly-once processing.
You can inspect the source, examples and limitations before adapting it.
A decision framework: what would I use for your workflow?
Here is the practical choice I would make.
| Your use case | Starting approach | | --- | --- | | Repeated low-stakes Slack notifications | Remove Duplicates with stable event ID | | Duplicate lead intake in a small CRM | Event dedupe plus CRM lookup/upsert and failure review | | Multiple lead sources with overlapping IDs | Normalize source-scoped IDs, then dedupe | | High-volume events with concurrent workers | Unique event-key reservation in durable storage | | Payments, refunds, inventory, account entitlements | Durable ledger plus destination-side idempotency and recovery | | Business events that may arrive out of order | Event identity plus version/order-aware reconciliation |
What matters is not how complicated the n8n diagram looks.
It is whether a duplicate, failure, or retry can produce a harmful result or silently lose legitimate work.
If you're still learning these patterns, my n8n learning-curve guide explains why data contracts, API behavior and error paths matter more than memorizing every node. For examples across CRM and operational automations, see my n8n work.
Frequently asked questions
Will the Remove Duplicates node remember IDs across webhook executions?
It is designed to compare against a history of prior executions when you select Remove Items Processed in Previous Executions. Verify persistence and clearing behavior for your specific n8n version and environment.
Is the ID stored for just a few seconds?
Not by default as a short-lived seconds-based cache. Its history is bounded by stored items and node configuration. Do not confuse an entry-count history with a configurable expiration timer.
Do I need two separate webhooks?
No. The normal integration is one existing webhook with a duplicate-checking step inside it.
Can I put it after my CRM node?
You can, but that defeats the point if creating the CRM record is the harmful repeated action. Put the gate before the side effect, while designing a recovery strategy for any side effects that fail after the key was recorded.
What happens when the sender supplies no event ID?
Do not silently invent a different random ID for each delivery. Reject or quarantine the event, or create a provider-specific deterministic identity when the sender's contract supports it.
Will this remove duplicate CRM contacts that are already in the database?
No. It filters repeated webhook deliveries. Cleaning and merging existing CRM records requires a separate identity-matching and data-quality process.
Does the node catch two identical webhooks arriving at exactly the same moment?
Do not rely on it as a cross-worker transactional lock. For important concurrent work, use a database uniqueness constraint or a destination API with explicit idempotency behavior.
Can two different events update the same lead?
Yes. In fact, that may be correct. The same lead can create, update, book, cancel, or close across multiple real events. Deduplicate the event, not every action involving the same lead.
What if the first execution fails after its ID was saved?
The retry can be suppressed even though the side effect never completed. That is precisely why critical flows need durable states, safe replay and destination-side idempotency.
Is a 200 response proof that my downstream action finished?
No. n8n may return its standard 200 fallback when the workflow ends without running a Respond to Webhook node. A success acknowledgement and a business completion receipt should not be treated as identical.
Is the workflow suitable for Stripe payments?
The basic demonstration is not a substitute for Stripe's verified event handling and a durable payment-processing design. Use Stripe's signing verification, documented duplicate-event guidance, and operation-specific idempotency safeguards. Never treat the demo as a payment ledger.
How do I start without overengineering?
Take one webhook that has actually caused duplicate work. Identify the provider's stable event ID. Add Remove Duplicates before the repeated action. Test first-seen, duplicate, invalid, failed-after-recorded, and concurrent cases. Then decide whether the business risk requires stronger storage.
Final takeaway
For most people learning n8n, preventing duplicate webhook deliveries starts with a very small change:
One webhook. One stable event ID. One Remove Duplicates step before the actions that matter.
But a more reliable automation asks a second question:
If the first attempt partially succeeds, fails, or times out, can the system safely finish the work without repeating the damage?
That is the part worth engineering carefully.
A workflow is not reliable merely because it avoids doing something twice.
It is reliable when it can recognize events, carry out the right business operation, explain the result, and recover when the world does not behave like a happy-path demo.
Sources and technical references
The product behaviors and provider-specific recommendations above are based on documentation rather than an original benchmark. The SQL and CRM architecture examples are explanatory patterns, not a verified end-to-end production implementation.
- n8n: Remove Duplicates node — previous-execution history, key comparison, scope and limits.
- n8n: Remove Duplicates examples — practical node configuration and item behavior.
- n8n: Webhook node — receiving HTTP requests and response configuration.
- n8n: Respond to Webhook — response-node behavior and standard HTTP 200 fallback.
- n8n: Execution history — retrying executions and diagnosing failures.
- n8n: Execution-data management — storage and retention considerations.
- Stripe: Receive webhook events — retries, event ordering, duplicate events, signature verification and acknowledgement behavior.
- GitHub: Webhook best practices — delivery identity, verification and response behavior.
- GitHub: Handling failed webhook deliveries — different retry/redelivery behavior.
- PostgreSQL: INSERT and ON CONFLICT — database-backed unique event-key reservation.
- Ahmad Yar Automation Lab: Webhook Replay Shield — inspectable example, tests and limitations.
