Preventing duplicate API actions in n8n webhooks
Learn how to design an n8n webhook workflow that uses a stable delivery ID, persistent tracking, and an atomic database claim to prevent repeated side effects during retries and concurrent deliveries.

Checked against the cited sources on .
Why webhook deliveries can repeat
A webhook is a delivery mechanism, not a promise that an event will arrive exactly once. A provider may send the same event again after a timeout or as part of its retry process. Your first workflow execution might even have completed the intended API action while its response failed to reach the provider. From the provider’s perspective, retrying can still be reasonable; from your workflow’s perspective, repeating the action could create a second ticket, payment request, message, or database record.
The safe design assumption is therefore that every delivery may be repeated. The workflow needs to recognize whether it has already accepted a particular delivery before it reaches any irreversible action. This property is usually called idempotency: processing the same delivery more than once should not multiply its intended effect.
Duplicate handling is especially important when two copies arrive nearly simultaneously. A simple lookup followed by an insert can appear correct during sequential testing but fail under concurrency. Both executions can perform the lookup before either inserts a record, conclude that the delivery is new, and then both trigger the protected action.
Choose and validate a stable delivery ID
Start by identifying the delivery identifier documented by the webhook provider. It should represent the delivery consistently across the retry behavior you need to handle. For example, GitHub documents that its delivery header remains the same when a delivery is deliberately redelivered. That makes the header suitable as an idempotency key in that specific context, but you should confirm the equivalent guarantee in the documentation for every other provider.
Extract the identifier immediately after authenticating and validating the request. Treat a missing, empty, or malformed identifier as an error instead of constructing a replacement from mutable payload fields. A hash of selected payload values may accidentally merge distinct events or treat modified representations of one event as different deliveries.
If one workflow receives events from several providers, use a composite identity such as provider plus delivery ID. You may also include an account or endpoint identifier when the provider only guarantees uniqueness within that scope. Store the raw delivery ID for investigation, but avoid relying on an identifier beyond the scope its provider documents.
Sources: S5
Persist delivery IDs across n8n executions
The record of accepted deliveries must survive beyond one workflow execution. n8n’s Remove Duplicates node can compare incoming items with data retained from earlier executions, which makes it useful for learning the basic deduplication pattern. Its stored history is bounded and configurable, however, and the supplied documentation does not establish transactional protection between simultaneous executions. Use it to explore repeated inputs, not as evidence of a concurrency guarantee.
n8n Data Tables offer another persistent option. They can hold structured information across workflows and support conditional lookups, row insertion, and upsert operations. A useful delivery record could contain the provider, delivery ID, event type, received time, processing status, and identifier of the resulting external record. Those fields create an audit trail for debugging.
The supplied Data Tables documentation does not establish support for an atomic uniqueness invariant under concurrent writers. If preventing two simultaneous executions from both claiming the same delivery is a strict requirement, use storage whose documented database constraints and conflict behavior provide that guarantee. Set retention according to the provider’s documented retry or replay window rather than assuming a universal duration.
Make the protected write atomic

In PostgreSQL, define the delivery identity as NOT NULL and protect it with a unique constraint. A unique constraint ensures that two rows cannot carry the same protected value. If uniqueness depends on more than one field, place the constraint across the complete identity, such as provider and delivery ID.
Claim the delivery with one INSERT operation that handles a uniqueness conflict. Do not build the critical path as “look up the ID, then insert if absent,” because those are separate operations with a race between them. PostgreSQL’s ON CONFLICT behavior can resolve competing inserts atomically when backed by an applicable unique constraint or unique index. Independent database errors are still possible and should follow a separate failure path.
The workflow should branch on the result of this atomic claim. The execution that inserted the new delivery record owns the right to continue. An execution that encountered the existing key recognizes a duplicate and skips the protected action. Place ticket creation, outbound messages, payment-related calls, and other irreversible operations only after ownership has been established.
This pattern guarantees one tracking row for the protected key under the documented database conditions. It does not automatically make a later external API request transactional with the database insert. Design status fields and recovery procedures for cases in which the claim succeeds but a downstream request fails.
Return success for recognized duplicates
A recognized duplicate is usually an already accepted delivery, not a new processing failure. After the atomic insert reports a conflict, route the execution around the side effect and return a successful acknowledgement when that matches the provider’s documented delivery protocol. For providers that retry failed responses, this avoids signaling that already accepted work should be delivered again.
Keep genuine failures distinct. An unavailable database, invalid delivery identifier, or authentication failure should not be reported as a harmless duplicate. Record enough context to distinguish a new claim, a known duplicate, and an operational error without storing secrets unnecessarily.
Choose explicitly when a delivery becomes “accepted.” Claiming before the external action prevents two concurrent executions from performing that action, but a failure after the claim requires a recovery policy. Suggested approaches include recording a processing status for later review or implementing a carefully controlled retry path that still respects the original delivery key. These are design suggestions, not a complete transaction spanning an external service.
Test retries and concurrent deliveries

Test the invariant in your own n8n environment before connecting the workflow to a consequential API. As a suggested test sequence, first send the same delivery twice in succession. The first request should claim the key and reach the protected step; the second should follow the duplicate branch and still receive a successful acknowledgement.
Next, simulate a provider-style redelivery by sending an identical stable ID again. Confirm that changing irrelevant request details does not bypass the key. Finally, launch two requests carrying the same ID as close together as possible. Inspect the database and the external system, not just the workflow’s visible branch, and verify that there is one claim record and one protected action.
Add failure tests as well. Try a missing ID, a malformed ID, an unavailable database, and a downstream API failure after a successful claim. These suggested checks are not a validated testing instrument, but they expose the boundaries between request validation, atomic ownership, duplicate acknowledgement, and recovery. They also make workflow debugging more concrete because each result corresponds to a deliberate state transition.
Practice the pattern in n8n
A practical learning workflow can begin with a Webhook node, followed by request authentication and extraction of the provider’s documented delivery ID. The next step attempts the atomic database claim. A successful insert proceeds to a harmless practice action, while a uniqueness conflict goes directly to a successful duplicate response. Operational database errors follow an error path rather than being mistaken for duplicates.
Build the first version with a visible audit record and a reversible side effect. Then replay the same request, inspect execution data, and run the concurrent test. Once the branches behave as intended, document the identifier’s provider-specific scope, the retention rule, and the recovery procedure for an interrupted first execution.
The node arrangement described here is editorial implementation guidance based on the supplied component behaviors; it is not a documented end-to-end reference workflow or evidence of measured learning outcomes. The central lesson is simple: persistent history can identify earlier work, but preventing concurrent duplicate actions requires an atomic claim enforced by the storage layer.


