Module 1: From Builder to System Owner
5. "At least once" delivery and the duplicate problem
Description
By the end of this lesson you'll be able to explain why the same event arrives twice, and why that isn't a rare accident but the rule almost every system that triggers your workflows is built on. You're going to learn about the guarantee webhooks, retries, and polling offer —it's called "at least once" delivery— and why its ideal opposite, "exactly once," is so hard to achieve that the industry decided not to chase it and to solve the problem from a different angle instead. Above all, you're going to understand that the duplicate isn't something you should prevent from happening, but something you need to learn to live with.
This matters because it completely changes the strategy. If you believe the duplicate is an exceptional failure, you're going to spend your energy trying to prevent it —and you're going to lose, because it isn't up to you—. If you understand that the duplicate is inevitable by design, you're going to spend your energy on the right thing: making sure that, when it happens, it doesn't cause harm. This lesson is the pivot that reorients the whole guide, from "how do I avoid duplicates" to "how do I make duplicates harmless."
Connection to the module: in lesson 4 you learned that every trigger creates an execution, and one question was left hanging: why does a trigger send the same event twice? This lesson answers it. It's the piece that explains the origin of the duplicate, while lesson 4 explained its mechanics (two executions that both charge). Together, the two complete the picture: triggers deliver at least once (this lesson), and n8n turns every delivery into an execution that runs the effects (lesson 4). Lesson 6 takes this and puts it alongside the other failure modes; lesson 7 gives you the conceptual tool —reads vs. effects— to know which duplicates matter.
The courier who prefers to over-deliver
Let's start with an image, because the underlying idea is simpler than it sounds.
Imagine a courier service that delivers important packages —legal documents, say— and has one golden rule: never, ever fail to deliver a package. Losing a legal document is catastrophic; delivering one twice is a minor annoyance. Given that asymmetry, how does the courier behave?
The courier knocks on the door and delivers the package. They wait for you to sign the receipt. If you sign, great, they leave satisfied. But if you don't sign —because you weren't there, because the connection to their device failed, because it took too long— the courier has a problem: they don't know whether the package arrived or not. And given their golden rule, they do the only sensible thing: come back tomorrow and deliver another package. They'd rather you get two identical documents than risk you getting none.
From your side, you received the document twice. It's annoying. But notice the courier didn't make any mistake: they did exactly the right thing according to their golden rule. The duplicate isn't a bug in the courier; it's the logical consequence of a reasonable rule —"better too many than too few"— when the confirmation gets lost.
That's how almost every system that triggers your workflows works. A webhook provider, a payment gateway that notifies you, an API that retries: all of them would rather send you one extra event than risk losing one. And all of them, when they don't get your confirmation in time, resend. The duplicate you see isn't an accident; it's the price, deliberately accepted, of never losing an event.
The three possible guarantees
To name this precisely, distributed systems engineering talks about three levels of delivery guarantee. It's worth knowing all three, because the vocabulary shows up in the documentation of almost any service you integrate with.
"At most once." The system sends you the event a single time and doesn't retry. If it gets lost along the way, it's lost: you never find out. The advantage is there are never duplicates. The disadvantage is there can be losses, and for most businesses losing an order is worse than processing it twice. Almost nobody uses this guarantee for anything important.
"Exactly once." The dream: the event reaches you once and only once, never lost, never duplicated. It's what everyone wants. And it's —this is the surprising part— extraordinarily hard to truly achieve in a distributed system, for a reason we're about to see. So hard that most services don't even attempt it.
"At least once." The event reaches you once or more than once, but it's never lost. It's the document courier's guarantee: they'd rather over-deliver than lose something. It is, by a huge margin, the most common guarantee in the real world, because it combines the one non-negotiable thing (not losing events) with something actually feasible to implement. The price you pay is that you have to be ready to receive duplicates.
Here's the central fact of the lesson, the one I want you to burn into memory:
The vast majority of your workflows' triggers offer "at least once" delivery. That means receiving the same event twice isn't the exception: it's a possibility guaranteed by design.
It's not that providers are careless. It's that they chose —correctly— not to lose events, and the price of that choice is that sometimes they over-deliver. The duplicate is a feature of the contract, not a defect.
It's worth seeing where this guarantee shows up in writing, because it's no secret: serious providers document it. If you open the webhook documentation for almost any payment gateway, messaging platform, or event system, you're going to find, somewhere, a line like "events are delivered at least once, so your endpoint should be idempotent" or "be prepared to receive the same event more than once." They don't say it as an apology; they say it as a specification, because they know resending is part of their job and deduplicating is part of yours. When you integrate a new service, looking for that phrase in its docs is a good first step: it confirms which guarantee you're dealing with and, often, tells you which field to use to deduplicate.
Why "exactly once" is almost impossible
Let's pause on this, because understanding why an easy "exactly once" doesn't exist is what convinces you to stop looking for it.
The problem is the same as the courier's, and it comes down to one sentence: when a confirmation gets lost, the sender can't tell "the message didn't arrive" apart from "the message arrived but the confirmation got lost." Both cases look identical from their side: they sent something and got no response. Faced with that uncertainty, they only have two options, and both are imperfect:
- Don't resend. If the message truly hadn't arrived, you just lost it. → risk of loss (at most once).
- Resend. If the message had arrived, you just duplicated it. → risk of duplication (at least once).
There's no magic third option, because the sender has no way of knowing which of the two cases it is. The uncertainty is fundamental: it comes from the fact that the confirmation travels over the same unreliable channel as the message, and that channel can fail in either direction.
Think of a phone call that drops. You're dictating a number to someone and the call drops right as you finish. Did they write it down or not? You don't know. If you call back and dictate it again, maybe they write it down twice. If you don't call back, maybe they didn't write it down at all. There's no way, from your side, to know which happened without extra information. Distributed systems live permanently in that "it dropped right at the end" moment.
So how do you achieve "exactly once" when you truly need it? Not by eliminating duplicates in delivery —that's impossible— but by accepting duplicates in delivery and discarding them on reception. The sender delivers at least once; the receiver recognizes "I already processed this event" and ignores the repeats. The net result, seen from outside, is "exactly once," but it's built on top of "at least once" plus a smart receiver.
And that smart receiver is you. Your workflow. This is the revelation that reorients the guide:
You're not going to make events arrive exactly once —that's not up to you—. You're going to make your workflow treat the second event for what it is: a duplicate it already processed, one it can safely ignore.
That —safely ignoring the duplicate, or making reprocessing it change nothing— is Module 2's idempotency. This whole guide is the construction of that smart receiver.
Where the duplicates in order-triage come from
Let's bring the theory down to the concrete case. In order-triage, the same order can arrive twice through several paths, and all of them are forms of "at least once." We already named three in lesson 1; now you truly understand them.
The webhook's provider retries. The online store —or the gateway, or whatever system triggers order-triage— sends you the order and expects your webhook to respond quickly with a "received." If your instance is slow, or there's a network blip, or the workflow takes a while to respond, the provider doesn't get your confirmation in time. True to its "at least once" guarantee, it resends. Second trigger, second execution.
Polling overlaps. Some workflows don't wait for a webhook; instead, they ask periodically "are there new orders?". If one query takes longer than the interval between queries, two queries can overlap and pull in the same order. It's another duplicate source, different from the webhook but the same family.
n8n retries. As you saw in lesson 4, if a node or an execution fails and a retry is configured —or someone retries by hand— the action runs again. Here the duplicate doesn't come from outside but from your own system trying to recover.
The human double-clicks. The humblest and most common one. A customer presses "confirm," the page is slow, they press again. Two triggers, one order in their mind.
Notice something: all four paths are different, but all of them produce the same result —the same event coming in twice— and all of them share the same underlying cause —someone, somewhere, chose to over-send rather than risk losing something—. That's why it doesn't make sense to fight each path separately. The right defense isn't "prevent the provider from retrying" or "prevent the human from double-clicking" —you control neither—. The right defense is a single one, downstream: your workflow recognizes the duplicate and doesn't process it twice, no matter which path it came through.
The duplicate at scale: why "rare" isn't "never"
It's worth doing a quick calculation, because it's the one that turns "this almost never happens" into "this happens often," and it's the one that justifies this guide's effort.
Suppose —it's a hypothesis, you'd need to measure your actual case— that only 1 in every 500 events gets delivered twice. That sounds negligible: 0.2%. With that number, it's easy to conclude "not worth worrying about."
Now multiply it by volume. If Cumbre processes 500 orders a day, that 1 in 500 means, on average, one duplicate charge per day. Three hundred-some customers overcharged per year, each one with their complaint call, their refund, and their hit to trust. What was negligible in one execution becomes a daily incident at the scale of a day.
This is the same arithmetic you saw in lesson 2 applied to partial failure, and it holds for every failure mode: an unlikely event in any single execution is routine when there are thousands of executions. That's why the system owner doesn't ask "how likely is this in one run?" but "how many times a month, given my volume?". The first question makes you dismiss the risk; the second makes you size it. And at almost any production volume, the duplicate stops being a theoretical rarity and becomes a line in the weekly report.
Worked example: the same event_id, twice
Let's go back to Cumbre's canonical event and follow it through when the provider retries.
At 09:12:03, the provider sends the order:
{
"event_id": "evt_8f2a91c4",
"order_id": "ORD-2041",
"customer_id": "CUST-118",
"amount": 2154.00,
"currency": "MXN"
}
Your webhook receives it, order-triage starts, and begins processing. But your instance is busy and takes four seconds to respond "received" to the provider. The provider expected a response within two seconds. At 09:12:05, without having received your confirmation, it resends the exact same event:
{
"event_id": "evt_8f2a91c4",
"order_id": "ORD-2041",
"customer_id": "CUST-118",
"amount": 2154.00,
"currency": "MXN"
}
What to expect. Two observations, and both matter.
First: the two events are identical, field by field. Same event_id, same order_id, same amount. There's no difference in the data that lets you tell "this is new" apart from "this is the resend." And yet there's a huge clue: the event_id is the same. That field —which in lesson 1 I said might look redundant— is exactly what the provider left you so you could say "I already saw this event." The provider can't avoid resending, but it does hand you the key to detect the resend. Whether you use that key is your job.
Second: as you saw in lesson 4, n8n creates two independent executions, one for each trigger. Neither knows about the other. With no protection, both run the six nodes, both charge the $2154. The customer sees two charges.
Here's the shape of the solution, even though we're not building it yet: if order-triage had a way to remember the event_ids it already processed, the second execution could ask "have I already seen evt_8f2a91c4?", get a "yes," and stop before charging. The provider keeps resending —that doesn't change— but the second resend no longer causes harm. That's turning "at least once" into an "exactly once" effect, from your side.
And remember the trap from lesson 4: that "have I already seen this event_id?" has to be done carefully, because two overlapping executions can ask at the same time and both get a "no." That's why Module 2's solution isn't simply "check before charging," but something more robust. For now, seeing the clue —the event_id— and the shape of the defense is enough.
Responding fast helps, but doesn't solve it
There's a technique that reduces resends, and it's worth knowing so you don't mistake it for the solution. It isn't the solution —the solution is idempotency— but it's a good complement, and understanding why it isn't enough sharpens your judgment.
Remember why the provider resends: because it didn't get your confirmation in time. From there comes a reasonable idea: confirm quickly, before doing all the work. Instead of your workflow processing the whole order —reading the customer, classifying, creating, charging, sending— and only telling the provider "received" at the very end, you can respond "received" (a 200 OK) as soon as the order comes in, and then process it. n8n allows exactly that: there's a way for the webhook to respond immediately to the provider while the workflow keeps working on its own.
The logic is good. If you confirm to the provider in a hundred milliseconds, it's much less likely their patience runs out and they resend. That eliminates part of the resends: the ones caused by your processing taking longer than their wait time.
But notice why this reduces resends without eliminating them:
- A network blip can lose your
200 OKeven if you sent it fast. The provider doesn't receive it and resends anyway. - The human who double-clicks isn't waiting for any confirmation from you: they fire twice before you even respond.
- Overlapping polling doesn't depend on your response time.
- n8n's internal retries happen downstream, after you've already confirmed.
In other words: responding fast plugs one of the duplicate sources —the provider's wait-time one— but leaves all the others open. And there's a subtler, almost paradoxical nuance: if you respond "received" before processing, and the processing then fails, you told the provider everything went fine when it didn't. Responding fast changes the contract from "I confirm when I'm done" to "I confirm I received it," and that has its own consequences that Module 5 revisits with the outbox pattern.
The conclusion is the usual one in this guide: no technique that acts on delivery eliminates the duplicate, because delivery is "at least once" by design. Responding fast is good hygiene —it reduces the noise— but the real protection lives downstream, in making sure processing the duplicate doesn't cause harm. Use both: respond fast to reduce the avoidable resends, and make your effects idempotent to survive the unavoidable ones.
The strategy shift this lesson asks of you
It's worth making the mental pivot explicit, because it's the most important one in the whole module.
Before this lesson, it was natural to think of the duplicate as a problem to prevent: "I have to keep the same order from coming in twice." That framing leads you to fight with providers, to put locks on the webhook, to treat every retry as an enemy. And it's a losing fight, because "at least once" delivery isn't a defect you can patch: it's the very contract the systems you depend on operate under.
After this lesson, the duplicate is a fact of life to absorb: "the same order is going to come in twice, and my job is for the second time to not cause harm." That framing puts you in the right place: designing your effects so repeating them is safe. You don't fight the provider; you assume their behavior and protect yourself downstream.
We could sum it up like this: stop trying to make the event arrive just once. Start designing so it can arrive as many times as it wants, with no consequences. That's the heart of the owner mindset applied to duplicates, and it's the doorway into Module 2.
And notice the peace of mind this shift brings. Chasing "exactly once" in delivery is a race against network physics that you never fully win: there's always one case, one blip, one timeout that slips past you. Designing to absorb duplicates is a bounded problem: you identify your effects, you put a defense on them, and you're covered no matter how many times the event arrives. You trade an infinite fight for a finite task. That is, ultimately, the practical reason the industry chose "at least once plus an idempotent receiver" instead of "exactly once": not because it's more elegant, but because it's the only one you can actually finish.
Common mistakes
Treating the duplicate as a provider bug (conceptual). What happens: a double charge shows up, it's traced to a webhook resend, and the conclusion is "the provider is broken, we need to tell them to stop resending." Why it happens: from your side, the resend looks like someone else's mistake. But the provider resends on purpose, because its guarantee is "at least once" and it would rather over-deliver than lose your event. How to spot it: if your plan for avoiding duplicates includes "ask the provider not to retry," you're on the wrong track. How to fix it: accept the resend as part of the contract and protect yourself downstream. A serious provider should resend; the one that doesn't resend is the one losing events, and that's worse.
Chasing "exactly once" in delivery (conceptual). What happens: someone invests effort in guaranteeing the event arrives exactly once —configurations, edge deduplication, locks— aiming for there to never be a second trigger. Why it happens: "exactly once" sounds like the clean, definitive solution. How to spot it: if your design tries to prevent the second trigger from arriving, instead of preventing it from causing harm, you're chasing the impossible. How to fix it: accept that the second trigger is going to arrive and move on to making it harmless. Real "exactly once" is built at reception —your workflow ignores the duplicate— not at delivery.
Confusing "hasn't happened to me" with "won't happen" (conceptual). What happens: a workflow has been in production for months with no reported duplicate, and it's concluded that the problem doesn't apply to this case. Why it happens: duplicates depend on conditions —latency, network blips, load spikes— that don't occur every day, so a workflow can run for a long time without hitting one. How to spot it: if your only evidence that you're safe is "it hasn't happened yet," you don't have evidence, you have luck. How to fix it: the absence of duplicates in the past says nothing about the future; it says the conditions that produce them haven't lined up yet. Design as if it's going to happen, because at sufficient scale, it will.
Believing a unique order_id in your data already protects you (practical). What happens: someone sees that every order has a different order_id and assumes that, by itself, prevents duplicates. But when the same order arrives twice, both copies have the same order_id! Why it happens: "different orders have different ids" gets confused with "the system uses that id to detect repeats." The first is true; the second requires you to build the detection. How to spot it: ask yourself where, in your workflow, something compares the incoming id against the ones already processed. If the answer is "nowhere," the id isn't protecting you, it's just present. How to fix it: having a unique identifier is necessary but not sufficient; you need a place that remembers the ids already seen and logic that discards them. That's Module 2 (the key) and Module 4 (the place that remembers).
Exercises
Exercise 1 — Classify the guarantees. For each situation, say which delivery guarantee it describes: "at most once," "at least once," or "exactly once."
(a) A service sends you a notification and, if you don't confirm, resends it until you do. (b) A sensor sends a temperature reading every second and doesn't care if some get lost. (c) The result a customer experiences when your workflow ignores resends and processes each order exactly once.
See solution
(a) At least once. It resends until confirmed, so it never loses, but it can over-deliver. It's the document courier's guarantee and the most common one in webhooks and gateways.
(b) At most once. It doesn't retry; if a reading is lost, it's lost. And that's fine for this case: losing one temperature reading out of a thousand doesn't matter, and you don't want the cost of guaranteeing delivery of each one. The right guarantee depends on how serious it is to lose an event.
(c) Exactly once, but notice where: not in delivery —the service is still delivering at least once— but in the observed effect. The customer sees an "exactly once" result because your workflow, on reception, discarded the duplicate. That's exactly the guide's goal: an exactly-once effect built on top of an at-least-once delivery.
Why this works: the key is noticing that "exactly once" isn't a property of delivery you can demand, but a result you build by combining an "at least once" delivery with a deduplicating receiver. Confusing the two layers is the origin of most failed designs.
Exercise 2 — Find the deduplication clue. A provider sends you this event when someone cancels a subscription. If the provider resends because it didn't get your confirmation, which field would you use to recognize it's the same event and not a new cancellation? Justify it.
{
"event_id": "evt_c71b02",
"type": "subscription.canceled",
"subscription_id": "sub_4410",
"customer_id": "CUST-118",
"canceled_at": "2026-07-20T14:03:00.000Z"
}
See solution
The correct field is event_id: "evt_c71b02". It's the identifier of the event's delivery, and a resend of the same event carries the same event_id. If you keep a record of the event_ids you've already processed, you recognize the resend instantly.
Watch out for the temptation to use subscription_id or customer_id. subscription_id identifies the subscription, not the event: if that same subscription gets canceled, reactivated, and canceled again weeks later, you'd have two legitimate cancellation events with the same subscription_id, and if you deduplicated on that field, you'd ignore the second, real cancellation. event_id distinguishes "the same event resent" from "a new event about the same subscription," which is exactly the distinction you need.
Why this works: deduplication needs a key that's unique per event, not per business entity. event_id satisfies that; business ids (subscription, customer, order) don't always, because the same entity can generate several legitimate events. Choosing this key correctly is Module 2's central topic, and you just took the first step.
Exercise 3 — Rewrite the strategy. A coworker tells you: "I'm going to fix the duplicates by configuring the webhook to reject any order that arrives less than five seconds after the previous one from the same customer." Explain, using this lesson, why that strategy is fragile, and what the correct approach is.
See solution
The strategy is fragile for several reasons. First, it fights the symptom at the wrong boundary: it tries to prevent the duplicate from arriving, when the duplicate is going to arrive by design ("at least once"). Second, the five-second window is arbitrary and fails in both directions: a provider can resend after eight seconds (and the duplicate slips through the filter), and a legitimate customer can place two real, distinct orders within three seconds (and the second, genuine one gets rejected). Third, it uses "same customer" as the criterion, when two distinct orders from the same customer are perfectly normal.
The correct approach doesn't look at time or at the customer, but at the event's identity. Instead of "reject whatever arrives too close together," it's "recognize the event_id I already processed and don't process it again." That defense doesn't depend on guessing a time window or assuming the same customer won't repeat: it relies on an identifier that precisely distinguishes a resend of an event from a new one. And, done right, it protects the effect even if two executions arrive overlapping.
Why this works: any defense based on "time between events" or "same customer" is a heuristic that guesses, and heuristics fail at the edges. A defense based on the event's identity doesn't guess: it knows. That's the leap from a fragile solution to a robust one, and it's the difference between the builder's instinct and the system owner's judgment.
Summary and next step
In this lesson you understood why the same event reaches you twice. Almost every one of your workflows' triggers offers "at least once" delivery: they'd rather over-deliver than lose an event, so when they don't get your confirmation in time, they resend. The duplicate isn't an accident or a provider bug; it's the price, deliberately accepted, of never losing an event. Its ideal opposite, "exactly once," is almost impossible at delivery time, because when a confirmation gets lost, the sender can't tell "it didn't arrive" apart from "it arrived but I wasn't told," and faced with that uncertainty it can only risk loss or risk duplication.
The consequence reorients the whole guide: you're not going to make events arrive exactly once, because that's not up to you. You're going to make your workflow, on reception, recognize the duplicate and not process it twice —building an "exactly once" effect on top of an "at least once" delivery—. You saw the four paths through which an order reaches order-triage twice —a retrying provider, overlapping polling, n8n retrying, a human double-clicking—, that all of them share the same underlying cause, and that's why the right defense is a single one, downstream, resting on the event_id the provider leaves you precisely for that purpose.
Before moving on you should be able to: explain in one sentence what "at least once" means and why it's the most common guarantee; explain why "exactly once" is almost impossible at delivery and where it can actually be built; and name the field in Cumbre's event you'd use to recognize a resend.
You already have the duplicate's origin (this lesson) and its mechanics (lesson 4). What's left is seeing the duplicate alongside its siblings, because the double trigger is only one of four failure modes a system owner needs to map. Lesson 6 gives you all four —partial failure, double trigger, out-of-order delivery, and upstream schema change— and a method for listing them against a concrete flow, which is the direct step toward the audit the module closes with.
Resources
- Webhook node — n8n Docs — the docs page for the node that receives events; check how it responds to the provider, because a fast response reduces (though doesn't eliminate) timeout-related resends.
- Respond to Webhook node — n8n Docs — the node you use to control when and how you confirm to the provider; useful for understanding the race between your confirmation and their resend.
- Handling API rate limits — n8n Docs — about retries and waits on outgoing calls; shows the other side of the same phenomenon, when you're the one retrying against a service.
- Idempotency and at-least-once delivery — general reference — a reference entry on idempotency; useful for seeing that "at least once plus an idempotent receiver" is a standard distributed engineering pattern, not an idea exclusive to n8n.