Module 2: Idempotency: Making Repeats Not Duplicate
1. Introduction: repeating without causing harm
Description
By the end of this lesson you'll be able to explain, in a single sentence, what idempotency is and why it's the central piece of a reliable automation system. You're going to have the full map of this module's eight lessons —from the ground-up definition to the project where you make a real step idempotent— and you're going to know exactly which Cumbre problem each one solves. You're also going to recognize, looking at the order-triage workflow you already met in the previous module, exactly where the wound is that the second charge slips through.
This matters for a reason you already saw in module 1 and that's worth keeping fresh: in n8n 2.0, an event can arrive more than once. The provider that triggers your webhook retries if it doesn't get a response in time. The customer double-clicks. n8n's own engine retries a step that failed halfway through. None of that is a bug on your part; it's how distributed systems work. Delivery is "at least once," not "exactly once." And if your workflow treats every arrival as if it were new, every repetition turns into a second charge, a second email, or a duplicate record. Idempotency is the property that makes repeating safe. It is, literally, what separates a workflow that works in the demo from a system that survives in production.
Connection to the module: this lesson doesn't teach you to build anything yet. It's the map. Here you pick back up the problem module 1 left open —Cumbre's webhook firing twice and charging twice—, define in one sentence the tool that solves it, and get the route you're going to travel. Lesson 2 defines idempotency in depth, with the elevator button and the light switch. Lessons 3, 4, and 5 give you the three mechanical pieces: the key that identifies the event, the upsert that doesn't duplicate in the database, and the header that doesn't duplicate at an API. Lesson 6 warns you about the subtlest trap of all —"check then act"—, 7 brings all this into AI agent territory, and 8 pulls it all together in a project you can defend in an interview.
The problem we left open: Cumbre's second charge
Before defining anything, let's go back to the exact spot module 1 left us at, because this entire guide revolves around a single scene.
Cumbre is a Latin American wholesale distributor of coffee and tea. It sells to about 400 cafés and small shops, and being a small team, it automates everything it can. One of its central workflows is called order-triage, and it does three things, in this order:
Webhook ──► AI Agent ──► HTTP Request
(receives (classifies the (writes to the CRM
the order) order: priority, and triggers the
channel, urgency) charge and the email)
The Webhook receives the order when the online store triggers it. The AI Agent node reads it and classifies it —decides priority, detects the channel, flags whether it's urgent—. And the HTTP Request node takes that decision and executes the effect: it creates the order record in the CRM and triggers the charge (the customer charge) along with the confirmation email.
In a perfect world, every order comes in once, gets classified once, gets charged once. That world doesn't exist. This is what happened on an ordinary Tuesday:
Cumbre's online store fired the webhook with order ORD-2041. n8n's response took a little longer than usual —the AI Agent took three seconds to classify—. The store, which expects a confirmation within two seconds, assumed the delivery had been lost and fired the same order again. Now order-triage ran twice with the very same ORD-2041. Two classifications. Two writes to the CRM. And the part that really hurts: two charges. The customer got a duplicate charge and two confirmation emails. Someone at Cumbre had to discover the double charge, fight with the payment gateway to reverse it, and apologize to the customer.
What matters about this scene is that nobody wrote a bug. The workflow is "fine" in the sense that every node does what it says it does. The problem is that it was designed assuming every order arrives exactly once, and that assumption is false. Module 1 taught you to see that falseness —"at least once" delivery, the failure modes, the distinction between reads (operations safe to repeat, like querying a piece of data) and effects (operations dangerous to repeat, like charging)—. This module teaches you what to do about it.
To keep the data we're working with in mind, here's order ORD-2041 exactly as it reaches order-triage through the webhook. It's the same canonical Cumbre item used throughout the guide, and you're going to see it, with variations, in every lesson of this module:
{
"order_id": "ORD-2041",
"customer_id": "CUST-118",
"customer_name": "Luna Coffee",
"channel": "web",
"created_at": "2026-07-14T09:12:00.000Z",
"status": "pending",
"currency": "MXN",
"amount": 1780,
"line_items": [
{ "sku": "CF-ARA-500", "product_name": "Arabica Coffee 500g", "quantity": 12, "unit_price": 148.5 },
{ "sku": "TE-CHM-100", "product_name": "Chamomile Tea 100g", "quantity": 6, "unit_price": 62 }
]
}
Keep two fields in mind, because they're going to be this module's protagonists: order_id, which identifies the order, and amount, the amount to be charged. The identifiers are in English on purpose —order_id, not id_pedido— because that's the convention across the whole ecosystem and the real tech market; the prose is in English, the data is in English too, here in this locale.
Where the two triggers come from: the three sources of the duplicate
Module 1 already named them; it's worth recalling them here because idempotency exists precisely to shield you against all three, and none of the three is a mistake of yours that you can simply "fix."
The provider's retry. The system that triggers your webhook —the online store, a gateway, another platform— expects a response within a certain window. If your workflow takes too long to respond, or if the response gets lost on the way back, the provider assumes the delivery failed and sends the same event again. It's correct behavior on their part: they'd rather over-deliver than under-deliver. This is, by far, the most common source, and it's the one that hit ORD-2041.
The human double-click. A customer presses "confirm order," doesn't see an immediate response, and presses again. Or an employee, nervous because the screen froze, fires the same action twice. Two identical events seconds apart. You don't control this from n8n.
n8n's own retry. When a node fails halfway through —a network drop right when the HTTP Request already sent the request but before receiving the response— n8n can retry that step. And here's the treacherous part: maybe the request did reach the gateway and created the charge, but the response got lost, so n8n thinks it failed and retries, creating a second charge. The retry, which is a reliability mechanism, becomes a source of duplicates if what you're retrying isn't idempotent. Module 6 comes back to this in depth.
The practical conclusion for all three is the same: you can't close every door a duplicate walks through. You can reduce some of them —responding quickly to the webhook lowers the provider's retries— but there's always a gap left. That's why the winning strategy isn't closing doors; it's making it not matter how many times the same event comes in. That's idempotency.
The idea in one sentence: idempotency
Here's the definition you're going to carry through the whole module, and that lesson 2 is going to take apart calmly:
An operation is idempotent if running it many times leaves the same result as running it once.
Think of it with an elevator's floor button. You're on the ground floor, you want to go to floor 5, and you press the button. It lights up. Because the elevator's taking a while, and because we're impatient humans, you press it four more times. Do those five presses call five elevators? Do they take you to floor 25? No. The button registers "this person is going to floor 5" and that's it. The first press changed the state; the other four changed nothing. The result of pressing it five times is identical to pressing it once. That's idempotency.
Now compare it to a button that isn't idempotent: "add to cart" on a poorly built store. You click it five times and end up with five units of the same product. Every click added one. The result of five clicks is different from the result of one. That button is not idempotent, and that's why good stores protect it —they disable the button after the first click, or count how many you've already added—.
Cumbre's charge is like the "add to cart" button: every time it runs, it adds a charge. Our job in this module is to turn it into an elevator button: so the second, third, and fifth execution recognize "I already charged this order" and do nothing new. The order gets charged once, no matter how many times the webhook arrives.
Notice a nuance we're going to repeat a lot, because it's half the point: idempotent does not mean "doesn't get run twice." The webhook is going to fire twice, and you can't always prevent that. Idempotent means nothing bad happens when it runs twice. We're not fighting the repetition; we're making it harmless.
Worked example: the same workflow, before and after
Let's look at order-triage in its two versions, without building anything yet. Just so you see where we're headed.
The fragile version, the one that charged twice:
Webhook ──► AI Agent ──► HTTP Request (POST /charges)
always creates a new charge
The HTTP Request node makes a POST request to the payment gateway saying, in essence, "create a charge of 1780 pesos for customer CUST-118." Every time it runs, the gateway creates a new charge, with a new identifier. The gateway has no way of knowing this charge and the previous one are "the same"; to it, they're two distinct requests that arrived a minute apart. It does what you asked: two charges.
The idempotent version, the one we're headed toward:
Webhook ──► AI Agent ──► Code ──► HTTP Request (POST /charges)
(calculates the with the
idempotency_key) Idempotency-Key header
A new Code node shows up whose only job is to calculate a stable idempotency key for this order —an identifier that's going to be identical on the first arrival and on the second, because it's derived from the order's content, not from when it arrived—. And the HTTP Request now sends that key in a special header, Idempotency-Key, that serious payment gateways know how to read.
What to expect with the idempotent version. The first time ORD-2041 arrives, the gateway sees an Idempotency-Key it's never seen before, creates the charge, and stores "I already used this key, and the result was this charge." The second time it arrives —with the same key, because it's derived from the same order— the gateway recognizes the key, doesn't create a second charge, and returns you the same result as the first time, as if it had just done it. For your workflow, it's transparent: it receives a successful response both times. For the customer, there's a single charge.
Put in a table, the contrast is the entire guide in four cells. Imagine the webhook arrives twice with ORD-2041:
| After the 1st arrival | After the 2nd arrival | |
|---|---|---|
| Fragile version | 1 charge, 1 email, 1 CRM row | 2 charges, 2 emails, 2 rows |
| Idempotent version | 1 charge, 1 email, 1 CRM row | 1 charge, 1 email, 1 row (no change) |
The bottom row is this module's destination: the second arrival runs in full —we don't block it— but leaves no new effect. It runs "empty," like the second press of the elevator button.
Don't worry about the details yet. All I want you to notice is three things.
First: the solution wasn't preventing the webhook from arriving twice. It still arrives twice. The solution was making the second arrival not create a second effect.
Second: a new piece showed up, the idempotency_key, and everything depends on that key being the same on both arrivals. If we calculated it wrong —for example, using the arrival time, which is different each time— the second call would have a different key, the gateway would see it as new, and we'd be back to two charges. Choosing that key well is lesson 3, and it's subtler than it looks.
Third: this only works because the payment gateway cooperates —it knows how to read the Idempotency-Key header—. When the API doesn't cooperate, or when the effect is writing to your own database, idempotency has to be built a different way: with an upsert. That's lesson 4, and it's the one you're going to use the most.
Idempotency isn't "exactly once"
There's a confusion worth clearing up early, because otherwise the whole module feels like a half-solution.
When someone discovers the duplicate problem, their instinct is to ask for an "exactly once" guarantee: that the system, somehow magically, ensures every event gets processed once, no more, no less. It sounds like what you want. The problem is that "exactly once" delivery in distributed systems is, in the strict sense, impossible to guarantee. The network can always drop at the exact moment you don't know whether the effect happened or not. That's a well-known, well-established result in systems design, not a limitation of n8n.
What you can build —and it's what you actually want— is the combination of two things: "at least once" delivery (the event can arrive several times, and we accept that) plus idempotent processing (processing it several times leaves the same result as processing it once). The combined effect of the two is, for all practical purposes, "exactly once": the customer sees a single charge. But notice how it's achieved. It isn't achieved by preventing the repetition —that would be true "exactly once," the impossible one—. It's achieved by accepting the repetition and neutralizing it.
Think of it this way: instead of building a door that never lets the same person through twice —a door that doesn't exist—, you build a room where it doesn't matter how many times the same person walks in, because there's only one chair with their name on it and they always sit in the same one. That's this whole module's philosophy, and it's more humble and more robust than the "exactly once" fantasy.
Why this is the heart of the guide
It's worth stating clearly why this module, module 2, is the whole guide's center of gravity, and not just one module among others.
Everything that comes after rests on idempotency. Module 4's deduplication —the record that keeps count of which events you've already processed— exists so you can apply idempotency when the API doesn't give you a header. Module 3's contracts make sure the idempotency key one workflow passes another has the right shape. Module 5's coordination —the outbox pattern, fan-out— depends on every coordinated effect being idempotent, because otherwise coordinating badly duplicates in a chain. And Module 6's retries are only safe if what you're retrying is idempotent; retrying an effect that isn't is, precisely, how duplicates get manufactured.
Put the other way around: if you leave this module mastering idempotency, the rest of the guide is learning where to store the state and how to coordinate; but you already have the fundamental property. If you leave without mastering it, everything else gets built on sand.
That's why we're going to go slowly and with plenty of analogies. There's no rush. One concept at a time.
This module's map
Here are the eight lessons and what each one solves. It's worth coming back to this table after finishing each lesson so you don't lose the thread.
| Lesson | What it solves | The piece of Cumbre it touches |
|---|---|---|
| 2 | Exactly what idempotency is, and which operations already are by nature and which aren't | Why the charge isn't idempotent and a CRM query is |
| 3 | How to choose the key that identifies "the same event" across retries: natural vs. synthetic | Choosing ORD-2041's idempotency_key |
| 4 | The upsert: insert-or-update by key instead of blindly inserting | Turning the CRM write into an upsert by order_id |
| 5 | The Idempotency-Key header for APIs that support it, and the pattern for the ones that don't | Making the charge idempotent at the gateway |
| 6 | The check-then-act trap: why "look it up, and if it doesn't exist, create it" fails | The subtle bug that survives testing and blows up in production |
| 7 | Idempotency for an AI Agent's actions: when the agent calls a tool with an effect | Making Cumbre's AI Agent not charge twice on a retry |
| 8 | Project: take a step that creates records and make it idempotent, then test it | The full order-triage, re-run without duplicating |
Notice the order, because it isn't arbitrary. First the what (lesson 2): the clean definition, so you can recognize an idempotent operation when you see one. Then the three tools in dependency order: the key (3) is the raw material for everything else; the upsert (4) is the mechanism for your own data; the header (5) is the mechanism for third-party APIs. Then the warning (6), because the naive solution —two nodes, "check and create"— is so tempting and so broken that it deserves its own lesson. Then the application to the trendy case (7), agents. And finally the project (8), which introduces nothing new: it pulls together the previous six pieces into a deliverable.
If it helps to picture it: lessons 3, 4, and 5 are the three tools in the same toolbox. 3 is "how you give each event a unique name." 4 is "how you save it without duplicating at home." 5 is "how you ask for it without duplicating at someone else's house." 6 is the warning label stuck on the box.
A note on what you're NOT going to build here
So you know where you stand, two honest boundaries.
This module doesn't build the deduplication store. You're going to make one operation idempotent with this module's tools. The persistent record that remembers, across different executions and over weeks, which events you've already processed —the deduplication ledger— is module 4. Here you're going to use the idempotency the API itself gives you (via its header) and the database itself gives you (via its upsert); module 4 teaches you to build it yourself when neither one hands it to you.
This module doesn't operate in production. Safe retries, alerts when something truly fails, and reproducing a duplicate bug with n8n 2.0's replay engine are module 6. Here you design correctness; there you monitor it. It's the boundary the full guide states at the end.
I'm saying this because it's easy, when learning idempotency, to want to solve everything at once. You don't need to. The safe operation first; the state and the operation, later.
If a metaphor helps for placing the six modules: this module's idempotency is learning that a single switch doesn't electrocute anyone no matter how many times you flip it. Module 3 (contracts) is agreeing on what voltage goes in and out of each switch. Module 4 (data model) is the central panel that remembers which switches have already been flipped. Module 5 (dependencies) is coordinating several switches to flip in the right order without stepping on each other. And module 6 (retries and alerts) is the system that warns you when a switch has actually burned out. The whole building rests on the first brick —a safe switch— and that brick is what you put in place in these eight lessons.
Common mistakes
Believing idempotency means "doesn't run twice" (conceptual). What happens: someone understands the problem as the double execution and spends all their energy avoiding it —puts a lock, disables retries, prays the provider doesn't retry—. Why it happens: it's the intuitive reading, and it isn't entirely wrong; reducing duplicate executions helps. But it's a defense that always has cracks: you don't control the provider's retries, or the customer's double click, or a network drop that makes n8n retry. How to spot it: if your plan for the second charge is "I'm going to make sure the webhook doesn't arrive twice," you're fighting the wrong battle. How to fix it: change the goal. Don't prevent the repetition; make it harmless. An idempotent system assumes everything is going to arrive twice and is designed so it doesn't matter. That's this whole module's mindset.
Confusing "no error" with "no duplicate" (conceptual). What happens: the workflow gets tested, it runs twice, neither run throws a red error, and it's concluded to be fine. Why it happens: a duplicate effect almost never fails loudly. The second call to the gateway is a perfectly valid request that returns 200 OK; the second INSERT to the CRM runs with no problem and creates a new row. The harm isn't an error, it's one success too many. How to spot it: don't look for whether there was an error; count the effects. After running twice, how many charges are in the gateway? How many rows in the CRM? How to fix it: adopt, from now, this module's testing criterion, which lesson 8 formalizes: the idempotency test isn't "it ran with no error," it's "I ran it twice and there's exactly one effect."
Wanting to solve everything in the wrong module (conceptual). What happens: while learning idempotency, someone tries to build the persistent record of processed events, the alert, the retry, and the coordination all at once. They get overwhelmed and finish none of them. Why it happens: the topics are related and it's natural to see them together. How to spot it: if making one charge idempotent makes you feel like you first need an audit table, an alerting system, and a dead-letter queue, you're mixing modules. How to fix it: stay within this module's scope —one operation, one key, one upsert or one header— and trust that state (module 4), coordination (module 5), and operations (module 6) come later, with their own tools.
Exercises
Exercise 1 — Find the wound. Go back to the fragile order-triage diagram (Webhook → AI Agent → HTTP Request). In one or two sentences, say which of the three nodes causes the harm when the workflow runs twice, and why the other two, even though they also run twice, don't leave a permanent problem.
See solution
The node that causes the permanent harm is the HTTP Request, because it's the one that executes an effect: it creates a charge at the gateway and triggers an email. Every time it runs, it produces a new change in the outside world —one more charge, one more email— and those changes don't undo themselves.
The Webhook runs twice, yes, but receiving an order twice leaves no harmful trace by itself; it's just data coming in. The AI Agent also classifies twice, and classifying is essentially a reasoned read: it produces a decision (priority, urgency) but changes nothing outside. Classifying the same order twice wastes a bit of compute, but it doesn't overcharge or send an extra email.
Why this works: you're applying module 1's distinction —reads vs. effects— to a concrete case. The danger of repetition lives in the effects, not the reads. This whole module is focused on making effects safe, and that's why HTTP Request is the protagonist.
Exercise 2 — Idempotent or not. For each of these everyday operations, decide whether it's idempotent (repeating it leaves the same result as doing it once) or not, and explain in one sentence why:
(a) Turning off a room's light with a switch. (b) Serving yourself a spoonful of sugar in your coffee. (c) Setting a TV's volume to 15 with the remote (the one with a numeric keypad, not the up/down arrows). (d) Raising a TV's volume with the "volume +" arrow.
See solution
(a) Idempotent. Turning off a light that's already off leaves it off. The final state is "off," whether you do it once or ten times. It's the "set the state to a fixed value" type.
(b) Not idempotent. Every spoonful adds sugar. One spoonful sweetens it; five spoonfuls ruin the coffee. The result depends on how many times you do it. It's the "increment" type.
(c) Idempotent. Typing "15" on the keypad sets the volume to 15, no matter how many times you type it. The final state is "volume = 15." It's, again, "set the state to a fixed value" —the same pattern as the elevator's floor button—.
(d) Not idempotent. Every press of "volume +" raises it by one. Five presses raise it by five. It's "increment," just like the spoonful of sugar.
Why this works: the line separating (a) and (c) from (b) and (d) is exactly the line separating what's idempotent from what isn't. Setting a state to an absolute value ("make it 15," "leave it off") is idempotent. Modifying the state relative to what was already there ("add one," "add a spoonful") is not. In lesson 2 you're going to see this same distinction in database and API operations, and it's the mental tool you're going to use the most.
Exercise 3 — Rebuild the map. Without looking back at the "This module's map" table, write from memory what each of the seven following lessons (2 through 8) solves, one sentence each. Then compare and mark the ones you missed.
See solution
(2) What idempotency is and which operations already are by nature. (3) How to choose the key that identifies the same event across retries, natural or synthetic. (4) The upsert: insert-or-update by key in your database instead of blindly inserting. (5) The Idempotency-Key header for APIs that support it, and what to do with the ones that don't. (6) The "check then act" trap and why it isn't enough. (7) Idempotency for an AI Agent's effectful actions. (8) The project: making a step that creates records idempotent, and testing it by re-running.
Why this works: if you rebuilt at least five of the seven, you've already internalized the module's progression, which goes from the definition (2) to the three tools (3, 4, 5) to the warning (6) to the application (7) to the project (8). The ones people usually miss are 3 and 6, which are the most conceptual until you see them in code.
Summary and next step
In this lesson you picked back up the problem module 1 left open: Cumbre's order-triage workflow, which, when the webhook fires twice with the same ORD-2041, ends up creating two charges and two emails, with nobody having written a bug —the workflow simply assumed every order arrives exactly once, and that assumption is false—. You learned the definition governing this whole module: an operation is idempotent if running it many times leaves the same result as running it once. You saw it with the elevator's floor button —pressing it five times doesn't call five elevators— against the "add to cart" button, which adds one per click. And you saw, without building it yet, what order-triage looks like in its idempotent version: a stable key calculated in a Code node and an Idempotency-Key header that makes the second arrival not charge again.
The most important thing you're walking away with is a change of goal: we're not going to prevent things from happening twice —we can't always—; we're going to make happening twice harmless.
Before moving on to lesson 2 you should be able to: define idempotency in one sentence; explain why Cumbre's charge isn't idempotent and a CRM query is; and tell apart "setting a state to a value" (idempotent) from "modifying the state relative to what was there" (not idempotent).
Lesson 2 takes that definition and calmly takes it apart. You're going to see why certain operations are already idempotent by nature and you don't even have to do anything, which ones never are and need protecting, and where the HTTP methods —GET, PUT, DELETE, POST— that you already use every day fit into this, without knowing that half of them were already giving you idempotency for free.
Resources
- Idempotency — MDN Web Docs Glossary — the formal definition of idempotency applied to HTTP methods, short and precise. It's the canonical reference for the term and the foundation of lesson 2.
- Webhook node — n8n Docs — the node that triggers
order-triage; worth reviewing how it responds and what happens when the sender retries. - Error handling — n8n Docs — the general framework of what n8n does when a step fails and retries, which is one of this module's three sources of duplicates.
- Idempotency — Stripe API reference — how a real payment gateway implements idempotency with a header; we're going to study it in detail in lesson 5. Always verify the exact header name and current conditions here.