−Idempotency means that requesting the same operation repeatedly has the same effect as applying it once. If the response to an order request is lost, the caller cannot easily tell whether no order was created or whether the order exists and only the response went missing. Identifying the request and returning its stored result lets the caller recover the result without creating another order.
An identical request body does not necessarily mean the same operation: a user may want to order the same product twice. The example below therefore combines an authenticated user identifier with a request key chosen by the caller. Reusing that key with different content is an error. AWS on idempotent API design
[Image]
The diagram shows the sequence between create_order and db below. Both writes belong in one transaction so that an order cannot remain without its request record, or a request record without its order.
Creating one order and returning its result again
This standalone example uses Python 3.12 and SQLite from the standard library. owner identifies a user who has already passed authentication and authorization checks, and item is the entire request payload in this example. It uses one connection to an in-memory database, so the records disappear when the program ends.
import sqlite3
db = sqlite3.connect(":memory:", isolation_level=None)
db.executescript("""
CREATE TABLE orders(id INTEGER PRIMARY KEY, item TEXT NOT NULL);
CREATE TABLE requests(
owner TEXT NOT NULL, request_key TEXT NOT NULL,
item TEXT NOT NULL, order_id INTEGER NOT NULL,
PRIMARY KEY(owner, request_key)
);
""")
def create_order(owner, request_key, item):
db.execute("BEGIN IMMEDIATE")
try:
row = db.execute(
"SELECT item, order_id FROM requests WHERE owner=? AND request_key=?",
(owner, request_key),
).fetchone()
if row is not None:
if row[0] != item:
raise ValueError("KEY_REUSED_WITH_DIFFERENT_REQUEST")
order_id = row[1]
else:
order_id = db.execute(
"INSERT INTO orders(item) VALUES (?)", (item,)
).lastrowid
db.execute(
"INSERT INTO requests VALUES (?, ?, ?, ?)",
(owner, request_key, item, order_id),
)
db.commit()
return order_id
except Exception:
db.rollback()
raise
if __name__ == "__main__":
first = create_order("alice", "request-1", "book")
again = create_order("alice", "request-1", "book")
assert first == again
assert db.execute("SELECT COUNT(*) FROM orders").fetchone()[0] == 1
print(first, again) # 1 1
db.close()
The first call stores the order and request record; the next call reads the order_id for the same key. Sending pen with that key raises ValueError and rolls back the transaction. The composite primary key treats another user's identical key as a separate operation.
The except block cleans up the transaction and propagates the original exception. The code does not retry automatically or substitute a default result. BEGIN IMMEDIATE and the composite primary key enforce write ordering and request identity in this SQLite example. A service with multiple connections must still check for duplicates and record the effect in the same database transaction.
Retrying after a lost response
[Image]
(Original source)
Original diagram from Malcolm Featonby's Making retries safe with idempotent APIs, PDF page 4, printed page 3. The retry keeps the same identifier even when the first response never reaches the caller. The service recognizes the completed request and prepares a semantically equivalent response. Only the diagram was excerpted; its notation and arrows are unchanged. Original PDF, © Amazon Web Services.
Preventing duplicate effects and reusing responses are separate parts of the contract. A second call could return "already exists" while still applying the effect only once, but a caller trying to recover the first successful result needs to know which result it will receive. This example returns the stored order number.
Request key scope and retention
In production, decide which user and operation a key belongs to, how to determine whether request content is equivalent, how long records remain, and how to handle requests still in progress. Once a record is deleted, a late request can no longer be distinguished from a new one on that evidence alone. Design the valid retry window and retention policy together. The same key with a different payload must not be accepted as a successful duplicate. AWS on late requests and different intent
A database transaction does not also cover payments, email, or file writes outside that database. Use idempotency keys provided by the external service, or record an event for delivery with a Transactional outbox and design duplicate handling at the receiver. HTTP method idempotency likewise concerns the method's intended effect; response bodies and incidental logs may differ between calls. RFC 9110 §9.2.2
Related reading
Retry Pattern
Command Pattern
Optimistic concurrency control
Transactional outbox
+Press a button that sets the volume to 20 several times, and the volume stays at 20. Press a button that raises it by 1, and the value changes each time. Idempotence, often called idempotency in an API context, is the property of an operation whose first result remains unchanged when the operation is applied again.
Server developers often use the term when preventing duplicate payments or storing request keys, but mathematicians used it much earlier to describe repeated operations. The same name appears in function composition, HTTP retries, and merging the state of replicas. In each setting, the question is what gets repeated and which result we compare.
A name from powers
In §25 of Linear Associative Algebra, Benjamin Peirce uses the word idempotent and says that, unless otherwise specified, it means A² = A. He also defines nilpotent for an object that becomes zero when raised to a power. The names describe whether repeated multiplication yields the original element or zero. Benjamin Peirce was the father of Charles Sanders Peirce.
[Image]
(Original source)
The image shows printed page 8 (PDF page 20) of the 1882 edition, whose preface records that lithographed copies were first distributed in 1870. Edition record, preface.
Apply a function to its own result
Now put function composition in place of multiplication. A function f from a set to itself is idempotent if f(f(x)) = f(x) for every input x. Feed the first result back into the same function, and that result holds.
For integer absolute value, the first calculation has already removed a negative sign, so the next calculation returns the same value. Squaring takes 2 to 4, then 4 to 16.
def square(x):
return x * x
assert abs(abs(-3)) == abs(-3)
assert square(square(2)) != square(2)
print(abs(-3), square(2), square(square(2))) # 3 4 16
This runs in Python 3. Repeated calls to square(2) each return 4; that shows determinism, the same result for the same input. To check idempotence, feed that 4 into the next calculation. Reusing an input and applying an operation to its previous result are different tests.
This also connects to fixed points. A value y with f(y) = y is a fixed point. Every value produced by an idempotent function is immediately one: with y = f(x), we have f(y) = f(f(x)) = f(x) = y. Once the function produces a result, further applications leave it there.
A calculation that settles only after several repetitions has a different property. On nonnegative integers, repeated application of h(n) = n // 2 takes 8 through 8 → 4 → 2 → 1 → 0 → 0. It eventually reaches a fixed point, but the first result, 4, differs from the second, 2. A function is not idempotent across its whole domain merely because one input eventually stabilizes or another input happens to be a fixed point.
For a binary operation, compare what happens when both operands are the same. Set union satisfies A ∪ A = A. This has a different form from feeding a function its previous result, yet both laws say that repeating the same object in the operation makes no further change.
Which program state do we compare?
A volume-setting command transforms a state s into another state. Write that command as T, and we can ask whether T(T(s)) = T(s).
[Image]
Both rows start at volume 5 and apply the same command twice, showing how the resulting states diverge.
def set_volume(state):
return {**state, "volume": 20}
def increase_volume(state):
return {**state, "volume": state["volume"] + 1}
initial = {"volume": 5}
assert set_volume(set_volume(initial)) == set_volume(initial)
assert increase_volume(increase_volume(initial)) != increase_volume(initial)
print(set_volume(initial), increase_volume(increase_volume(initial)))
These functions return new dictionaries instead of operating a volume control. Both compute a new value from their input, but only set_volume is idempotent. A command that writes to a real device can cause a side effect and still be idempotent with respect to the volume we observe.
Property | Relationship to check | Example
Determinism | Does the same input give the same result? | Squaring an integer
Purity | Does the result depend only on the input, with no observable side effect? | A function that computes a new squared value
Idempotence | Does applying it to the first result leave that result unchanged? | Absolute value; setting volume to 20
Involution | Does applying it twice restore the original input? | Boolean negation
One Boolean negation gives a different value from two. Squaring also leaves 0 and 1 unchanged but fails the idempotence law at 2. A single output, or a few inputs where the law happens to hold, cannot establish the property over the full domain.
In a real program, we must also choose the state under observation. If the program records each button press separately from the volume, repeated setting may preserve the volume while changing that larger record each time. HTTP later chose the request's “intended effect” as its comparison for a related reason.
When other commands intervene
Idempotence describes reapplying the same operation. The order of different operations is a question of commutativity. Commands to set the volume to 20 and to 50 are each idempotent, but the final volume depends on which runs last.
Composing idempotent operations does not always produce another idempotent operation. On pairs of integers, let F(x, y) = (x, x) and G(x, y) = (y + 1, y). Applying either F or G twice has the same result as applying it once. Yet repeating a sequence of F followed by G changes the pair again:
Application | State
Initial | (0, 0)
F then G | (1, 0)
Run the same pair again | (2, 1)
Here is the table's example on integer pairs in Python 3. It applies each function repeatedly and then repeats the sequence of both functions.
def F(point):
x, _ = point
return x, x
def G(point):
_, y = point
return y + 1, y
start = (0, 0)
once = G(F(start))
twice = G(F(once))
assert F(F(start)) == F(start)
assert G(G(start)) == G(start)
print(once, twice) # (1, 0) (2, 1)
If two idempotent operations also commute, their composition is idempotent: expand two applications of the composition, put equal operations together by changing their order, and apply each idempotence law. When a retry repeats a whole workflow, its combined effect needs its own check.
Another user can change the state between attempts. A client sets the volume to 20, another user changes it to 50, and then the first client retries its earlier command: the volume returns to 20. The setting command remains idempotent, but preserving the other user's change requires a separate mechanism, such as a version condition or concurrency control.
Retrying when no response arrives
A client sends an order request to a server and receives no response. The connection may have failed before the server received the request, or the server may have saved the order and the response may have disappeared on its way back. Sending the request again can create the order in the first case; if the server also creates a new order in the second case, there will be two.
In §3.2 of their 1984 paper Implementing Remote Procedure Calls, Birrell and Nelson use call identifiers and sequence numbers to distinguish duplicate call packets. A caller retransmits a packet to obtain a response, and the receiver decides whether to execute the same call again. Early RPC design treated those as separate actions. Paper and publication record, paper PDF.
On the web, §9.1 of RFC 2068, the 1997 HTTP/1.1 specification, distinguishes safe methods from idempotent ones. It separates a client's request to retrieve information from a request whose effect does not accumulate when sent again. The distinction helps a client decide which requests it can retry automatically after a broken connection. RFC 2068 §§8.1.4 and 9.1.
RFC 7231 in 2014 and RFC 9110 in 2022 retain the distinction. The table describes method semantics; server implementers must make their code preserve those effects across repeated requests. RFC 7231 §4.2.
Method | Safe? | Idempotent? | Why
GET and HEAD | Yes | Yes | The client requests retrieval
PUT | No | Yes | It specifies the state of the target resource
DELETE | No | Yes | It removes the association with the target
POST | No | No general guarantee | The resource and application define what creation or processing means
PATCH | No | No general guarantee | Repeated effects depend on the change and its format
RFC 5789 defined PATCH for partial modification in 2010 and explains that a particular PATCH request can be constructed to be idempotent. Assigning the value 20 and adding 1 to the current value have different effects when repeated, even though both are partial modifications. For changes dependent on a base version, it recommends conditions such as a strong ETag with If-Match. RFC 5789 §2.
A first DELETE can return 204 and a later DELETE 404 while the intended deletion effect remains the same. RFC 9110 allows per-request logs or history and notes that responses may differ. The client compares the server effect intended by the request. RFC 9110 §9.2.2.
Another user may change a resource between two GET requests, so their responses can differ too. Whether a response can be stored and reused depends separately on cacheability and freshness rules. That is why safety, idempotence, and cacheability have separate names. RFC 9110 §§9.2.1 and 9.2.3.
Request keys distinguish attempts at one order
If a server creates a new order every time a client says to add one, a retry creates another order. The client and server first need a rule for which requests count as attempts at the same operation.
[Image]
(Original source)
This diagram is from Malcolm Featonby's Making retries safe with idempotent APIs. The caller has not received the first response, so it retries with the same identifier while the service checks processing state in StateStore.
A customer may intentionally order the same item and quantity twice, so hashing the request body alone would collapse two legitimate orders into one. Instead, the caller supplies an explicit request key—reusing it when retrying the same operation and generating a new key for a separate order. The server scopes each key to the authenticated caller, rejects payload mismatches under the same key, and defines the retention window for stored records. AWS article.
This Python 3 example makes sequential calls on one thread in one process. The server supplies an authenticated caller identifier as owner; key and item are already validated strings. One dictionary entry stores the order contents and number together, and records remain in the dictionary for the run.
requests = {}
def create_order(owner, key, item):
identity = (owner, key)
if identity in requests:
previous_item, order_id = requests[identity]
if previous_item != item:
raise ValueError("KEY_REUSED_WITH_DIFFERENT_REQUEST")
return order_id
order_id = len(requests) + 1
requests[identity] = (item, order_id)
return order_id
first = create_order("alice", "request-1", "book")
retry = create_order("alice", "request-1", "book")
another = create_order("alice", "request-2", "book")
print(first, retry, another) # 1 1 2
The same caller, key, and item return the existing order number; a new key creates a separate order for the same item. Calling create_order("alice", "request-1", "pen") with changed contents under the same key raises ValueError. The (owner, key) pair separates keys belonging to different callers.
Because this example uses an in-memory dictionary, both orders and retry records vanish when the process exits. Production servers must also handle concurrent requests and crash recovery, which requires atomic commitment.
Suppose the server checks for a key before processing, then saves the order and key separately. Two concurrent requests can both see “no record” and each create an order. The server could also stop just after saving the order. It must commit the identifying record and the order effect as one atomic operation. The record's states and the API contract then express whether an attempt is still running, whether it can run again after failure, and how to return a previous success result.
The request key identifies the same operation while its record is retained. After the server deletes the record, it may lack the information to distinguish an old key from a new request. If the caller changes the key, the retry may also look like a separate operation. Both the retention period and the caller's key-use rule bound the guarantee.
A local database transaction commits the order and processing record in that database together. Reprocessing an external payment follows the identifiers and contract provided by the payment service. Idempotence describes a repeated effect; deduplication identifies an operation already attempted. An “exactly once” claim must say whether it covers message delivery, code execution, or the business effect.
The law also applies when replicas merge state
Shapiro, Preguiça, Baquero, and Zawirski's 2011 paper Conflict-free Replicated Data Types distinguishes sending state for merging from sending operations for application. Its state-based model uses commutative, associative, and idempotent merging together with monotonic state updates. Idempotence here helps when a state is transmitted more than once. Paper §§2.1–2.4.
[Image]
(Original source)
These are Figures 1 and 2 on PDF page 3. On the left, a replica transmits its state and the receiver merges it. On the right, a replica transmits an operation and the receiver applies it. The set example below shows a merge operation suitable for the left-hand model.
def merge(left, right):
return left | right
local = {"a"}
remote = {"b"}
combined = merge(local, remote)
assert merge(combined, remote) == combined
assert merge(local, remote) == merge(remote, local)
print(sorted(combined)) # ['a', 'b']
This is Python 3 set union. Merging the same remote state again leaves the result unchanged. Commutativity covers a change in merge order, and associativity covers how several states are grouped. To explain convergence, we also need the conditions that updates grow the state by adding elements and that each replica eventually transmits its updates.
The same paper's operation-based model assumes causal ordering and exactly-once delivery. Apply an “increment by 1” operation message twice, for example, and the value increments twice. The delivery assumptions for operation messages differ from the duplicate-tolerant merge in the state-based model. Paper §§2.3–2.4.
What each context compares
Context | What repeats | Result compared
Algebra | Multiplication of the same element | The element itself
Function composition | Applying the same function to its previous result | The function value
A program that changes state | Reapplying the same command | The state chosen for observation
HTTP | Retransmitting the same request | Its intended effect on the server
An API with request keys | Reprocessing the same operation identifier | Contracted business effect and returned result
State-based replication | Merging the same state again | The merged state
Mathematicians compare an operation's result; clients and servers compare effects after a retry; designers of replicated systems compare states after repeated merges. Naming that scope shows how storing request keys, specifying a state, and merging sets each provide idempotent behavior.