Idempotency: exactly once is about the effect, not the delivery
Retrying a request is not a fix — it is a way to make the effect happen twice. Measured: four retries without a key gave four charges, with a key one. Between the effect and the answer there is a window that cannot be closed: measured, the client sees a failure where the server has already done everything. And the order of working with the key decides it all: claim it before the work and one effect, write it afterwards and two.
Full technical treatment
TL;DR
An operation is called idempotent when it can be repeated with no additional effect once it has succeeded a first time. Retrying a request does not have that property by itself: a server that does not distinguish retries applies the effect again. The agreement that "this is the same operation" has to be created between client and server — and it is created by a key that the client generates per operation and sends with every attempt.
The main consequence: a client cannot tell "the request never arrived" from "it arrived and the answer was lost". Measured: the client got an empty answer while the effect on the server had already been applied. So it has to retry — and a retry without a key is expensive: four identical requests produced four effects, the same four with a key one.
Beyond that come the order, the limits and the remaining numbers. A key by itself guarantees nothing: measured on two simultaneous retries, claimed before the work it gives 1 effect, written after the work, 2. A new key per attempt protects against nothing: three attempts under one key, 1 effect; three attempts under three keys, 3. And "exactly once" exists only about the effect: anyone can repeat a delivery any number of times, and the only thing that can be guaranteed is that a repeat will not do the work a second time.
- a client sends a request over a network, and the answer may never reach it;
- a client that got no answer normally retries — itself or through a library;
- some operations must not happen twice: charging money, sending an email, creating an order.
- idempotency keys, "exactly-once", conditional inserts and the race between a check and a write;
- how retries with exponential backoff and jitter are built.
What this question is really about
The ladder looks like this:
- "What is idempotency?" — a warm-up on the definition.
- "How do you make a retry safe?" — the substance starts here.
- "What if the answer is lost after the effect has been applied?" — the central question of the topic.
- "Is exactly-once delivery possible?" — a trap.
- "Two retries arrived at once — what happens?" — a question about the order of working with the key.
- "Who generates the key?" — the question that shows whether the client was thought about.
The numbers come from running bench/idempotency/keys.py and
bench/idempotency/practice.py over loopback. An effect here is one increment of
a counter inside the service: the thing that must not happen twice. That is what
is counted — not the number of requests and not what the client saw.
Base: the server charged the money and the answer never arrived
Start with the story the whole topic grew out of.
A client asks a service to charge a hundred. The service charges it — the money is gone, everything worked — and sends back an answer. The answer never reaches the client: the connection broke, a timeout expired, a packet was lost. The client sees one thing: there is no answer.
What should it do? There are two options and both are bad. Not retrying: if the request never reached the service at all, the charge never happened and nobody will find out. Retrying: if the charge did happen, it happens a second time, and a hundred becomes two hundred.
The client cannot tell the two cases apart, and that is the crux of the whole topic. "The request never arrived" and "the request arrived, the effect was applied, the answer was lost" reach it in exactly the same shape — as the absence of an answer.
Since the fork cannot be resolved, it is stepped around: retrying is made safe. An operation is called idempotent when it can be repeated with no additional effect once it has succeeded a first time: the first application does the work, each later one does nothing and reports the same result. Then the client does not need to tell anything apart — it simply retries until it gets an answer.
Some operations are idempotent by themselves: "set the status to done" can be repeated as often as you like, the result is the same. Charging a hundred has no such property — two charges are two hundred. So the property has to be added from outside, and it is added with an idempotency key: the client invents a unique value for the operation and sends it with every attempt, and the service uses that value to recognise a retry and return the earlier answer instead of doing the work again.
And here is the question this lesson is about: what does the service have to do for that to actually work? "The service stores keys" is not yet an answer: what matters is what exactly it does with a key, and when.
That is already enough to answer the basic interview question. Everything below is about how many effects come out with a key and without one, about the window between the effect and the answer that a key does not close, and about who generates the key and when.
Mechanism 1: a retry is a second effect
Now the same thing in numbers, starting with what happens without any protection. Four identical requests — exactly what a client with retries from the retries-and-jitter lesson does:
1. A RETRY WITHOUT A KEY APPLIES THE EFFECT AGAIN
-------------------------------------------------
requests sent 4
times the effect was applied 4
what the client got each time ok #1 | ok #2 | ok #3 | ok #4
Four requests, four effects. And note the last line: the answers are different. That is an honest picture of what happened — from the server's point of view these were four separate operations, not one operation retried four times.
This is the root of the topic. A client that retries believes it is doing one thing; a server that does not distinguish retries does four. There is no agreement between them that this is one operation — it has to be created.
Mechanism 2: a key creates that agreement
The mechanism that fixes this we have already named: the idempotency key. There is no standard for it: what follows is a draft of the IETF httpapi working group whose term has expired, and it is quoted for its wording rather than as a norm. The wording is short and explains the whole mechanism:
An idempotency key is a unique value generated by the client which the resource
uses to recognize subsequent retries of the same request.
Two phrases carry everything: generated by the client and recognize subsequent retries. Test it on the same four requests:
2. THE SAME RETRIES WITH AN IDEMPOTENCY KEY
-------------------------------------------
requests sent 4
times the effect was applied 1
what the client got each time ok #1 | ok #1 (replayed) | ok #1 (replayed) | ok #1 (replayed)
Four requests, one effect. And all four got the same answer — the one issued the first time.
The service did not become cleverer and guesses nothing. It does exactly what the definition says: on seeing a familiar key it returns the stored answer instead of doing the work a second time. The draft states that part too:
The resource SHOULD respond with the result of the previously completed
operation, success or an error.
Note the "or an error". A retry returns the same result rather than trying to do better: if the operation failed the first time, it fails the same way the second. Otherwise the key would stop meaning "the same operation".
Mechanism 3: the window between the effect and the answer
Now the central question of the topic — the one the Base section opened with, only now visible in a measurement. The effect is applied on the server, the answer travels to the client — and between those two events there is a gap. What if the answer is lost inside it?
3. THE WINDOW: THE EFFECT HAPPENED, THE ANSWER DID NOT ARRIVE
-------------------------------------------------------------
what the client saw on the first try empty answer
times the effect was applied by then 1
what the client saw on the retry ok #1 (replayed)
times the effect was applied in total 1
Read it line by line. The line empty answer means zero bytes: the connection
closed without bringing anything. For the client that is a failure. The effect had
already been applied: the counter shows one.
And the client was right to retry. The run shows the second half of the picture: the effect happened, the answer is gone. The first half — "the request never arrived at all" — is not in the measurement, and that is exactly why the client is powerless: both halves reach it as the same empty result.
Hence the main conclusion of the lesson — and that is already reasoning rather than measurement. It is worth stating precisely: a side effect on the remote side and the client's knowledge that the answer was delivered cannot be tied together atomically — at least not without an additional protocol on top of a plain request and answer. However many acknowledgements you add to such an exchange, the last of them is itself a message that can go missing, so the gap between "done" and "the other side knows it is done" never disappears; it only moves. A key does not remove that gap: it makes a retry after it safe. The retry lands on the same key and gets the same answer, and the effect stays at one.
That is the exact meaning of "exactly once": what can be guaranteed is not the delivery but the effect. The delivery will be repeated by anyone, any number of times.
Mechanism 4: the client generates the key — once per operation
The fourth level is about who is responsible for the key and when it is born. The draft answers the first half directly:
Uniqueness of the key MUST be defined by the resource owner and MUST be
implemented by the clients of the resource.
The key is generated by the client, and that is the whole point. A server cannot derive the key from the request itself: by the time it has the request, there is nothing left to tell it apart from a retry. The quotation gives a definition rather than a prohibition: schemes where the server hands out a token in advance do exist — but there too the value reaches the server before the operation rather than together with it.
Here is what happens when a client makes a new key per attempt:
5. A NEW KEY PER ATTEMPT PROTECTS NOTHING
-----------------------------------------
three attempts, one key: effects 1
three attempts, a new key each: effects 3
total effects applied 4
Three attempts under one key: one effect. Three attempts under three keys: three. The service did not change between the halves at all; the third line of the block is their sum — the counter is shared by both halves: 1 + 3.
Hence the practical point this level gets asked about: a key is born with the intent, not with the attempt. A client that generates the key inside its retry loop has implemented the mechanism and gained nothing from it.
And hence why this cannot be solved on the server side. What counts as "the same operation" is known only to whoever intended it.
Deeper: the order of working with the key decides everything
Everything above describes what a key is for and where it comes from. The last level is about how the service handles it — and this is where an implementation most often turns out to be fake. A key by itself guarantees nothing; what matters is when it is written. Test it on two retries that arrive at the same moment:
4. TWO RETRIES AT THE SAME MOMENT, ONE KEY
------------------------------------------
key claimed before the work 1 effect from 2 requests
answers ok #1 | ok #1 (replayed)
key written after the work 2 effects from 2 requests
answers ok #1 | ok #2
The same key, the same two simultaneous requests, the same store. The only difference is the moment of writing — and it gives one effect against two.
Why. If the key is written after the work, there is a gap between the check "have we seen this key" and the write. Two requests both get through the check while neither has written yet — and the work is done twice.
Hence the rule worth answering with: the store has to claim the key, not remember it. The check and the write have to be one indivisible operation: an insert conditioned on "if this key does not exist yet", not "read, think, write".
In the run the indivisibility comes from a lock inside the process — which is enough, because both ends live in one. The rule about a conditional insert is the consequence: across several processes a lock would not help, and the store has to take the lock's role.
This is exactly where an implementation of idempotency most often turns out to be fake: the key is there, the store is there, and so is the race.
How to answer in an interview
Short answer: exactly-once delivery cannot be guaranteed, but the effect can. The client generates a key per operation, the server claims a place under that key before doing the work and stores the answer, and any retry gets the same answer instead of a second effect. Measured: four retries without a key, four effects; with a key, one.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
Three things separate a good answer. First, you name the window between the effect and the answer and say precisely what is impossible in it: tying the effect on the remote side to the client's knowledge that the answer was delivered, atomically — that cannot be done without an additional protocol on top of the exchange. Idempotency does not remove that uncertainty; it makes a retry after it safe. Measured: the client sees a failure where the effect has already been applied. Second, you talk about order: the key has to be claimed before the work rather than written after it; measured, the second gives two effects on two simultaneous retries. Third, you say the key is generated by the client, and explain why: by the time a request reaches the server, nothing distinguishes it from a retry.
What not to say: "we did exactly-once". That is a promise about delivery that nobody can make; the promise to make is about the effect — and that one can be checked.
Next they ask
How long should keys be kept?
Longer than the client can keep retrying. If a client uses exponential backoff capped at an hour while keys live for five minutes, a late retry arrives with an already-forgotten key and produces a second effect — the mechanism stops working in exactly the rare case it exists for.
Hence the right order: first find out how many retries the client makes and for how long, then choose a retention. That is not in the measurement — there the store is a dictionary with no expiry — but the connection is plain from how the key works.
What if a different request arrives under the same key?
That is a separate class of error, and it is solved by storing a fingerprint of the body alongside the key. If the key matches and the body does not, the honest answer is to refuse: the client made a mistake, and replaying somebody else's answer to it is not acceptable.
In this measurement the body is not checked at all: the subject there is how many times the effect is applied. But the danger is worth naming yourself, because a client that reuses a key by mistake will receive somebody else's result and not notice.
How is this different from an idempotent method?
In where the guarantee lives. An idempotent method is a property of the operation itself: "set the status to done" can be repeated any number of times because the result is the same. A key is needed for operations that have no such property: "charge a hundred" twice is two hundred.
So the first question in a review is not "where do we get a key" but "can the operation be restated so that no key is needed". "Set the balance to X" instead of "subtract Y" solves the problem with no store at all — provided such a phrasing is acceptable.
Where is the limit here?
Where the effect leaves your system. A key protects what your service does; if in the process it sent an email or called an external payment gateway, a retry inside your code will not do that a second time — but exactly the same problem now sits with the neighbour.
Hence what must not be promised: a chain of services does not become idempotent because the first one is. There is no chain in the measurement — there is one service there; this follows from the definition: a key recognises a retry only where it is read. So every link solves the problem for itself, and the key has to reach the place where the effect happens.
Common misconceptions
A retry is safe as long as the request has not changed
What has not changed is the request, not the effect. Measured: four identical requests without a key produced four effects, and the client got four different answers. For a server that does not distinguish retries these are four separate operations.
Exactly-once delivery can be achieved
It cannot: an effect on the remote side and the client's knowledge that the answer was delivered cannot be tied together atomically — not without an additional protocol on top of the exchange — and both halves of the picture reach the client as the same empty result. Measured: the client got zero bytes while the effect had already been applied. What can be guaranteed is the effect, not the delivery: a retry after that uncertainty lands on the same key and gets the same answer.
It is enough to remember the key after processing
No: there is a gap between the check and the write. Measured on two simultaneous retries: a key claimed before the work gave 1 effect, written after it, 2. The check and the write have to be one indivisible operation.
The server can generate the key
Derive the key from the request itself — it cannot: by the time the server has the request, nothing distinguishes it from a retry. The draft gives the definition: An idempotency key is a unique value generated by the client
. What counts as "the same operation" is known only to whoever intended it.
The key is conveniently generated in the client library as the request is sent
It matters most. Measured: three attempts under one key gave 1 effect, three attempts under a new key each gave 3, with the same service throughout. A client that generates the key inside its retry loop has implemented the mechanism and gained nothing from it.
Practice
Two exercises. Answer first, then check against the real output: in both, the correct answer is what the measurement script prints.
Practice · predict the output
without_key = retries(use_keys=False) with_key = retries(use_keys=True) late_key = simultaneous(claim_key=False) print(without_key) print(with_key) print(late_key)
Practice · estimate
Knowledge check
Four identical retries without an idempotency key. How many times is the effect applied?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- An operation is called idempotent when it can be repeated with no additional effect once it has succeeded a first time. Retrying a request does not have that property by itself: a server that does not distinguish retries applies the effect again. The agreement that "this is the same operation" has to be created between client and server — and it is created by a key that the client generates per operation and sends with every attempt.
- The main consequence: a client cannot tell "the request never arrived" from "it arrived and the answer was lost". Measured: the client got an empty answer while the effect on the server had already been applied. So it has to retry — and a retry without a key is expensive: four identical requests produced four effects, the same four with a key one.
- Beyond that come the order, the limits and the remaining numbers. A key by itself guarantees nothing: measured on two simultaneous retries, claimed before the work it gives 1 effect, written after the work, 2. A new key per attempt protects against nothing: three attempts under one key, 1 effect; three attempts under three keys, 3. And "exactly once" exists only about the effect: anyone can repeat a delivery any number of times, and the only thing that can be guaranteed is that a repeat will not do the work a second time.
In fact
- What has not changed is the request, not the effect. Measured: four identical requests without a key produced four effects, and the client got four different answers. For a server that does not distinguish retries these are four separate operations.
- It cannot: an effect on the remote side and the client's knowledge that the answer was delivered cannot be tied together atomically — not without an additional protocol on top of the exchange — and both halves of the picture reach the client as the same empty result. Measured: the client got zero bytes while the effect had already been applied. What can be guaranteed is the effect, not the delivery: a retry after that uncertainty lands on the same key and gets the same answer.
- No: there is a gap between the check and the write. Measured on two simultaneous retries: a key claimed before the work gave 1 effect, written after it, 2. The check and the write have to be one indivisible operation.
- Derive the key from the request itself — it cannot: by the time the server has the request, nothing distinguishes it from a retry. The draft gives the definition: An idempotency key is a unique value generated by the client. What counts as "the same operation" is known only to whoever intended it.
- It matters most. Measured: three attempts under one key gave 1 effect, three attempts under a new key each gave 3, with the same service throughout. A client that generates the key inside its retry loop has implemented the mechanism and gained nothing from it.
What is covered
- What this question is really about
- Base: the server charged the money and the answer never arrived
- Mechanism 1: a retry is a second effect
- Mechanism 2: a key creates that agreement
- Mechanism 3: the window between the effect and the answer
- Mechanism 4: the client generates the key — once per operation
- Deeper: the order of working with the key decides everything
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
1 SOURCE
- The Idempotency-Key HTTP Header Field (an expired IETF Internet-Draft)Source. There is no standard for idempotency keys — there is a draft of the IETF httpapi working group, and it has expired. The document says as much itself, in the mandatory IETF preamble: "It is inappropriate to use Internet-Drafts as reference material or to cite them other than as 'work in progress'". So this lesson takes a definition from it and takes no norms. Section numbers move between revisions of a draft, so they are not given below. What a key is: "An idempotency key is a unique value generated by the client which the resource uses to recognize subsequent retries of the same request". What to do with a retry: "The resource SHOULD respond with the result of the previously completed operation, success or an error". Who is responsible for uniqueness: "Uniqueness of the key MUST be defined by the resource owner and MUST be implemented by the clients of the resource".https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header