Build the mental model
Timeouts and retries create a new problem the moment you combine them: a client sends a request, the server receives it and actually processes it, charging a card or creating a record, but the response is slow enough that the client's timeout fires first.
The client sees a failure and, following the retry logic from the last lesson, sends the same request again. From the server's point of view, two identical requests just arrived, and it has no way to know the first one already succeeded unless you designed it to know.
Idempotency is that design property: an operation is idempotent when performing it multiple times has exactly the same effect as performing it once. Reading a resource is naturally idempotent; creating a new charge or order is not, each call is a brand new side effect by default.
Client generates a key
The client creates one unique idempotency key per logical operation.
Sent with the request
The key travels with the request, usually as a header.
Server stores it on first sight
The first time the server sees the key, it performs the operation and stores the result alongside it.
A duplicate returns the cached result
If the same key arrives again, the server skips the operation and returns the stored result instead.
Not every API supports this, so checking a provider's documentation for idempotency-key support before you rely on it is essential, not optional.
- Idempotency
- The design property where performing an operation multiple times has the same effect as performing it once.
IDEMPOTENCY KEY PREVENTS A DOUBLE CHARGE
----------------------------------------
CLIENT SERVER
------ ------
|-- POST /charge, key=key-abc -------->|
| | charges card,
| | saves result
| | under key-abc
| (response lost, client times out)
|
|-- POST /charge, key=key-abc (retry)-->|
| | key seen before!
|<-- SAME result, no new charge --------|Connect it to a real scenario
The function below models the server side of idempotency-key handling using a plain in-memory `Map` as the store, standing in for whatever a real backend would use, typically a database row with a unique constraint on the key.
The five scripted requests deliberately repeat two keys, `key-abc` and `key-xyz`, simulating exactly the timeout-then-retry scenario above, and include one unique key, `key-def`, that only ever arrives once.
Watch the `action` field in the output: the first time a key appears, the function performs the logical charge and stores the result, labeled `processed_new_charge`. The second time, no new charge happens; it returns the exact stored result, labeled `returned_cached_result`.
One detail worth noticing: the cached result for `key-abc` on request 2 is the identical object stored from request 1, not a freshly generated one. A buggy implementation that generated a new charge ID on every lookup would still look idempotent at a glance while silently creating a second real charge.
What happens without idempotency
On a payment endpoint with no idempotency handling, a timeout followed by a client retry can charge the customer's card twice for one purchase -- a real customer paying double for one order.
Try the working example
function processRequests(requests) {
const store = new Map();
const results = [];
for (const req of requests) {
if (store.has(req.idempotencyKey)) {
results.push({
id: req.id,
key: req.idempotencyKey,
action: "returned_cached_result",
result: store.get(req.idempotencyKey)
});
} else {
const result = { chargeId: `ch_${req.id}`, amount: req.amount };
store.set(req.idempotencyKey, result);
results.push({
id: req.id,
key: req.idempotencyKey,
action: "processed_new_charge",
result
});
}
}
return results;
}
const requests = [
{ id: 1, idempotencyKey: "key-abc", amount: 5000 },
{ id: 2, idempotencyKey: "key-abc", amount: 5000 },
{ id: 3, idempotencyKey: "key-xyz", amount: 1200 },
{ id: 4, idempotencyKey: "key-xyz", amount: 1200 },
{ id: 5, idempotencyKey: "key-def", amount: 750 }
];
console.log(processRequests(requests));request 1 (key-abc): processed_new_charge -> chargeId ch_1, amount 5000
request 2 (key-abc): returned_cached_result -> chargeId ch_1, amount 5000
request 3 (key-xyz): processed_new_charge -> chargeId ch_3, amount 1200
request 4 (key-xyz): returned_cached_result -> chargeId ch_3, amount 1200
request 5 (key-def): processed_new_charge -> chargeId ch_5, amount 7505-minute try-it
Add one more request to `processRequests`' scripted list that reuses the `key-def` key, and predict its `action` field before running the code to check.
One important caution
Regenerating the idempotency key on every retry -- a fresh key each time means the server can never recognize the duplicate
Assuming every API supports idempotency keys -- relying on it without checking the docs can leave you with zero protection
Idempotent Requests - Stripe API Docs — API Integration & Webhooks