Idempotency keys for unsafe operations
A key names one intended operation; a retained result lets repeats recover its outcome without repeating its effect.
Predict first
Before you read: predict
Two independent export intentions have the same body; each is resent once. The API retains keyed POST results throughout this run. nextKey() returns a fresh identifier; bodyKey(body) returns the same identifier for equal input.
Which client preserves both intentions without repeating either effect?
function clientA(body) { return () => post(nextKey(), body);}function clientB(body) { const key = nextKey(); return () => post(key, body);}function clientC(body) { const key = bodyKey(body); return () => post(key, body);}const begin = { A: clientA, B: clientB, C: clientC }[choice];const left = begin(body), right = begin(body);await left(); await left(); await right(); await right();The 30-second version
Generate a key per logical operation and reuse it on retries. Bind it to the request fingerprint. Claim that identity before work; reject conflicting reuse. Preserve the effect and response within one durable boundary, then replay that response. Concurrent requests need an explicit policy. Once the record expires, the old key may describe new work.
A key names one intended operation; a retained result lets repeats recover its outcome without repeating its effect.
How it works
Step 1 of 8
Analogy: A perfumer's sample docket
Imagine a fictional perfumer using numbered sample dockets. A customer requests one vial with a chosen scent formula. If the acknowledgement disappears, the customer repeats the docket number. The perfumer retrieves its completion receipt instead of blending another vial. A second intentional sample receives its own number, even when its formula is identical.
| Sample-room detail | API mechanism |
|---|---|
| Docket number assigned when the customer requests a sample | Client identity for one operation |
| Formula attached to the numbered docket | Request fingerprint |
| One perfumer takes responsibility before blending starts | Atomic claim |
| Another perfumer sees that the sample is underway | Active-duplicate policy |
| Completion receipt retained with the completed work | Stored response |
| Old dockets removed after the advertised period | Retention boundary |
Breaks down when: a perfumer can recognise a scent by smell. An API cannot infer intent from resemblance. Nor does a paper docket make a remote blending action transactional: an interruption between pouring and filing still requires reconciliation. This invented sample room explains the mapping, not real perfumery practice.
Contrasting case: the customer intentionally orders a matching vial for another recipient. A matching formula is not a duplicate instruction. The application must distinguish a deliberate new request from a resend; asking the server to infer that distinction from bytes loses intent. The AWS source explicitly distinguishes equal request parameters from equal caller intent.
In real terms: store identity and input binding as well as a durable result. Treat the storage lifetime, concurrency policy and effect boundary as parts of the API contract, not properties conferred by the header's spelling. The HTTP-method definition stays in its linked pack; a keyed POST adds an application agreement rather than changing POST's definition.
Examples, run for real
ExamHall: repeated final submit
Synthetic EXH-1850 repeats a candidate's final-submit request after uncertain acknowledgements. MR !2050 retains the operation key and the response record. The fixture sends three completed HTTP requests through the same loopback server and counts committed SQLite effect rows. It stores a submission marker only: a separate grading worker is outside this run, so the example makes no claim about broker delivery or downstream exactly-once processing.
example ExamHall responses=201,201,201 effects=1
The repeated statuses are observed outputs of this contract. HTTP idempotence alone does not require equal responses. Here the saved body also contains the same receipt identity, verified by the harness; a status-only assertion would miss a handler that created another record but happened to choose the same status code. The source for retained status-and-body replay is Stripe's contract.
Media streaming: correcting an export request
A viewer requests an export containing the lesson clip, then edits that request to name the recap clip while keeping its key. The fixture refuses the changed payload. The viewer starts a new export with a fresh key, which is accepted. This distinguishes correcting an already-bound operation from repeating it; validation is checked before returning the saved response, as required by the draft's payload-binding rule.
example streaming statuses=201,422,201 effects=2
These are deliberately bounded experiments. The server has one process; active claims are coordinated in its synchronous in-memory map, while completed effects and responses commit together in SQLite. The separate rollback experiment closes and reopens its database after an injected exception. Neither proves multi-host coordination, recovery of every crash state, nor atomicity with an external payment or grading service. Those require the real system's persistence and recovery design; AWS's ACID requirement is stronger than remembering a key in an evicting cache.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 6
The first export POST reaches an explicit work barrier. Before it is released, a matching POST reaches the handler.
Once both finish, which sorted response-status list and committed effect count does the fixture show?
Question 2 of 6
This server follows Stripe's stored-result contract: it retains the first response's status and body. A keyed create with input label original commits, but its socket closes before the reply arrives. A separate edit changes that receipt's current representation to renamed. The client repeats its create with unchanged input.
Which tuple is returned status, receipt label and total create effects?
Question 3 of 6
A synthetic archive API uses a virtual retention clock. A fresh key has expiry epoch 10. Requests with that key and unchanged body run at epochs 0, 9 and 10.
Which final status and total effect count does the last request produce?
Question 4 of 6
An export POST with key export and clip alpha is held before its effect. Before release, another POST arrives with key export and clip beta.
What status and effect count does the second request observe while the first remains held?
Question 5 of 6
Callers Blue and Red independently use key shared to export clip alpha. Blue then sends shared with clip beta. Every candidate implementation commits an effect and its receipt together; it compares the stored input when its unique lookup finds a row.
Which uniqueness rule accepts each caller's intended export while rejecting Blue's changed-input reuse?
Question 6 of 6
An endpoint execution failed at a dependency, committing status 500 and no business effect. Later the dependency recovers. A reviewer proposes a new branch for repeated requests carrying that retained key and payload.
Which branch implements the fixture's repeat handler?
Go deeper
Pro tip
Separate “work completed” from “caller received the reply” in a test harness. Inject a lost reply after commit and inspect both database state and the returned receipt on retry. This is a testing method, not a new retry policy; retry budgeting belongs to its linked pack. AWS
Sources
All fetched 2026-09-30 through agent-reach's Jina Reader path; complete source bodies with URL, read date and SHA256 are in sources/.
- Published October 2025; expired 18 April 2026; historical work in progress, not an RFC or claim of current standardisation. The fixture adopts its 409/422 choices explicitly.
- Stripe: Idempotent requests: saved status/body including endpoint-execution errors; parameter comparison; new execution after pruning. The fixture's virtual retention is synthetic, not Stripe's retention period.
- Malcolm Featonby, AWS Builders' Library: Making retries safe with idempotent APIs: caller-provided intent, equal parameters not necessarily duplicate intent, durable atomic recording, caller scoping and parameter mismatch.
Related lessons
Checked against the tools and docs as of 2026-09-30.