Lab: get charged three times for one order
Retry a request the way every HTTP client does, then add the one header that makes the retry safe.
The payment cards lesson said an Idempotency-Key is required on order, and required is easy to read as bureaucratic. This shows what the header is actually preventing, which is the most ordinary failure in distributed systems: a response that never arrived.
- 1
Model a charge endpoint with and without idempotency, then retry the way a client does after a timeout.
bashcat > /tmp/idem.py <<'EOF' charges, seen = [], {} def charge(amount, key=None): if key is not None: if key in seen: return f"replayed, no new charge (total {sum(charges)})" seen[key] = True charges.append(amount) return f"charged {amount} (total {sum(charges)})" print("no key: the response timed out twice, so the client retried twice") for _ in range(3): print(" ", charge(25)) charges.clear(); seen.clear() print("\nwith a key: same three attempts") for _ in range(3): print(" ", charge(25, key="order-abc-123")) EOF python3 /tmp/idem.py - 2
Three attempts, one order, and a seventy-five dollar difference.
textno key: the response timed out twice, so the client retried twice charged 25 (total 25) charged 25 (total 50) charged 25 (total 75) with a key: same three attempts charged 25 (total 25) replayed, no new charge (total 25) replayed, no new charge (total 25) - 3
Clean up.
bashrm /tmp/idem.py
The client was not buggy. A timeout tells you the response did not arrive and tells you nothing about whether the server acted, so retrying is the correct behaviour and the only safe way to do it is to make the second attempt recognisable as the same request.
- The key must be generated by the caller and reused across retries of one logical operation. Generating a fresh key per attempt is the same as having none.
- 1Claw scopes replay protection to a 24-hour window and compares a hash of the body, so the same key with a different body is a conflict rather than a silent replay.
- This is why an agent doing anything irreversible needs a stable key: an agent that retries on error is an agent that will eventually retry a purchase.
The dangerous version of this bug is invisible in testing, because it only appears when a response is lost. It shows up first in production, during an incident, when everything is already timing out.
The same reasoning covers bootstrap and spend-policy writes, which take the same header for the same reason. Anywhere a retry could duplicate an effect, the key is what makes the retry safe.
Check your understanding
3 questionsWhy did the client retry at all?
What happens if the client generates a fresh key per attempt?
Why is this especially important for agents?