Make every write safe to retry
Give every write an idempotency key so a retry has the same effect as one call.
Retries are not an edge case any more — they are the normal operating mode of every client, queue, webhook and agent loop you depend on. If a write is not idempotent, then somewhere in your system a timeout that was actually a success is charging a card twice or sending a second email. The client cannot distinguish 'it failed' from 'the response was lost', so the server has to.
What goes wrong: A payment captured twice because the first response timed out on a request that had already succeeded.
What agents change: An agent loop retries on its own initiative and does not know which of its calls already landed.
You are violating it when
- A retried request creates a second row.
- Your webhook handler has no dedupe on the event id.
- Nothing in your write path mentions an idempotency key.
The usual objection: That this is a payments concern. Anything a queue can redeliver or an agent can re-run has the same shape.
A write request needs a caller-provided identity that means “this exact operation,” not merely “this HTTP request.” The server records that identity with the result of the first completed attempt. When the same identity arrives again, the server returns the recorded result instead of performing the side effect again.
This protects the gap between doing the work and the caller learning that it was done. Timeouts, dropped connections, queue redelivery, webhook retries, and process crashes all create cases where the caller cannot know whether the write landed. The idempotency key moves that uncertainty to the server, where the system can check prior work and make the retry converge on one effect.
The misconception is that this is a payments concern. Payments make the damage obvious, but the shape is the same for creating accounts, sending notifications, provisioning resources, applying credits, opening support tickets, or mutating any external system. If the operation can be retried by infrastructure, it can be duplicated unless the write has a stable identity.
When an agent is making calls, retries are less visible and more frequent. The agent may re-plan, re-run a tool, or recover from a partial transcript without knowing which side effects already happened. Idempotency keys give those tool calls a durable memory boundary, so repeated attempts do not expand the blast radius.
Install it
npx klay practices add idempotent-writes.klay/practices/idempotent-writes.mdcreate# Idempotent writes The client cannot tell "it failed" from "the response was lost". So the server has to. Retries are not an edge case any more. Every HTTP client, queue, webhook deliverer and agent loop in your stack retries by default. If a write is not idempotent then somewhere in your system a timeout that was actually a success is charging a card twice. ## The mechanism The caller generates a key that is stable across its own retries — a UUID per logical operation, not per attempt — and sends it with the request:
The previews are the first lines of each file; the command writes them in full. Existing files are never overwritten.
How you know it stuck
npx klay practices audit reports this check for this practice:
idempotency-keys
Where this comes from
- Making retries safe with idempotent APIsBroadens the pattern to distributed systems using caller-supplied tokens beyond payment APIs.
- Idempotent requests | Stripe API ReferenceShows the concrete server behavior: save the first result and replay it for the same key.
- RFC 9110: HTTP SemanticsProvides the protocol-level basis for why automatic retries require idempotent semantics.
- 10 Tips for Building Resilient Payment Systems - ShopifyGrounds the risk in a production double-charge scenario caused by a lost response.
Questions
- Can I not just make the operation naturally idempotent?
- Better, when you can — a PUT of a full resource beats a key. The key exists for the operations that genuinely create something new each time.