Claude Skipped Idempotency Because I Never Asked. So I Made Asking Unnecessary.
The code broke on a retry I never mentioned, and even when I ask for the fix, the first version is usually the wrong shape. The answer was…
Claude Skipped Idempotency Because I Never Asked. So I Made Asking Unnecessary.
The code broke on a retry I never mentioned, and even when I ask for the fix, the first version is usually the wrong shape. The answer was to write the requirement down once, as a Claude Code skill, so nobody on the team has to remember to say it.

Frederick, Maryland
Ask Claude to build an endpoint that charges a card and creates an order, and you get clean code. A handler that reads the cart, calls the payment processor, writes the order row, and returns a 200 with the order ID. Retry logic on the outbound calls, error handling, the lot. I’ve watched it produce that in under a minute, and the code passes review. Then the mobile client times out at five seconds, sends the same request again, and the customer gets charged twice.
The endpoint did exactly what I described. I described charging a card and creating an order. I never said the words “do this at most once,” because in my head, that went without saying, and nothing in my head reaches the model unless I type it.
The pattern
Idempotency is the property that running the operation twice has the same effect as running it once. Reads have it for free; on writes you build it yourself. The standard tool is an idempotency key. The client generates a unique ID for the request, sends it in a header, and the server uses it to recognize a retry and hand back the original result instead of doing the work a second time. Stripe requires one on payment creation. Most internal APIs skip it, because the failure only appears when a request gets sent twice, and that almost never happens in local testing.
So the pattern is well understood, well documented, and absent from nearly every first draft an AI hands me. The reason matters because the same reason makes the harder parts come out wrong later.
Why it doesn’t show up on its own
The reason is structural. The model optimizes for code that matches the description, and idempotency is never in the description. It’s a property of how the endpoint behaves across many calls under bad network conditions, and the model is reasoning about the one call in the prompt. It has no picture of the client on a flaky connection that resends after a timeout. The duplicate is a runtime event with no trace in the source, so the model builds for the request it can see and stops there.
My own default works the same way. Hand me “charge the card and create the order,” and I give you a faithful, working implementation of that sentence. Nothing in it mentions a retry, so the code never handles one.
The interesting failures start when you ask
So you ask for it. “Make this idempotent with an idempotency key.” Now the failures get interesting, because the version I write is a correct-looking implementation of the wrong shape.
I reach for check-then-act first. Look up the key. If you don’t find it, do the work and store the key. That reads fine, and it has a race condition in it. Two retries arrive at nearly the same moment, both run the lookup, both find nothing, both do the work. You’re back to charging twice, except now there’s code sitting in front of the bug that looks like it prevents exactly that.
The fix is to claim the key first. Insert it as a new row and let the database’s unique constraint pick the winner. One request gets the insert, the rest get a constraint violation, and that violation is the signal that this is a retry. The check and the act collapse into one atomic step instead of two. I can write this correctly, but I write the racy version first about as often as not, because the racy version is the obvious reading of “check whether we’ve seen this key.”

Next, I get the return wrong. Preventing the duplicate charge is half the job. The retry still needs an answer, and the right answer already exists, the same 200 and the same order ID that the first request produced. What I tend to write throws a duplicate-key error or returns a 409 on the retry, which means a client that retried because it never saw the first response now gets an error instead of the order it actually created. The original outcome has to be stored against the key and replayed on the retry. That part is almost never in my first draft.
Then comes the failure in the middle. The first request claims the key, starts the work, and the process dies before it finishes. The key is sitting there marked in progress, the work was never completed, and every retry now sees a claimed key and refuses to move. The customer is locked out of an order that never happened. Handling this means the key needs states, not just presence: started, completed, maybe failed, with a stored response on completion and a TTL or a recovery path for the ones that get stuck. I have never once produced that in a first pass without being asked for it by name.
There’s a quieter version of all of this below the endpoint. A key at the API boundary stops the duplicate from creating two orders, but if the handler calls a payment processor, that call needs its own idempotency, or the key has to travel with it. Otherwise, you’ve deduplicated the order and double-charged the card anyway. I handle the layer you pointed me at and leave the one you didn’t.
The issue
None of this is the model being bad at code. Every piece it writes is locally correct. The trouble is that idempotency is a distributed-systems property, and the model reasons about the program, not the system the program runs inside. Retries, partitions, two requests with the same key in flight at once, a crash between two lines, these are conditions that exist in production and nowhere in the source. They sit in the head of whoever has run a system like this before and watched it fall over at 2am.
The problem sits in the ask, not in the model. I left the requirement out of the description, and nobody implements a requirement that never got stated. The fix most people reach for is vigilance: say “make it idempotent” on every endpoint, spell out the atomic claim, the stored response, the key states, the TTL, every single time, and hope every engineer on the team carries the same list in their head on the same days. Vigilance doesn’t survive a Thursday afternoon, let alone scale across a team. The better fix is to write the requirement down once, in a place where the model reads it without anyone asking.
The fix: a skill, written once
Claude Code has a mechanism built for exactly this. A skill is a folder holding a SKILL.md file, a few lines of YAML up top with a name and a description, then plain markdown explaining how you want something done. The description stays loaded all the time; when a task matches it, Claude pulls in the full instructions on its own, with no one invoking it or remembering to. The prompt stays “build an endpoint that charges a card,” and the requirement rides along anyway.
Mine lives at .claude/skills/idempotent-writes/SKILL.md and the description line does the targeting: it fires on any work touching an endpoint or handler that creates, charges, sends, or otherwise changes state. The body is the contents of this article compressed into instructions, and it's mostly the unhappy paths:
---
name: idempotent-writes
description: Apply when creating or modifying any endpoint, handler, or
job that performs a write with side effects (creates, charges, sends,
transfers). Ensures retry-safe, idempotent implementations.
---
# Idempotent writes
Any operation a client could send twice must be safe to receive twice.
1. Require an idempotency key on every side-effecting endpoint.
Client-generated, passed in an `Idempotency-Key` header.
2. Claim the key atomically. INSERT the key with state `in_progress`
under a unique constraint and branch on the result. Never
SELECT-then-INSERT; the check and the act must be one step.
3. Run the operation inside one transaction with the claim.
4. Store the full response against the key and mark it `completed`.
A retry of a completed key replays the stored response with the
original status code. It does not return 409 and does not re-run.
5. A retry of an `in_progress` key returns 409 with Retry-After.
6. Keys need a TTL and a recovery path. A request that dies after
claiming must not lock the key forever.
7. If the handler calls a downstream service with side effects,
propagate the key or use the downstream's own idempotency
mechanism. Deduplicating at the edge while double-charging
downstream is a failure.
8. Write a test that sends the same request twice concurrently and
asserts one side effect and two identical responses.
Eight rules, each one written after something broke. The atomic claim sits at rule two because without it, the racy check comes back every time. Rule four exists because the 409-on-retry mistake follows close behind. The TTL made the list after watching a stuck key lock a customer out of an order that never happened. Rule eight does the enforcement: a concurrent-duplicate test proves all the others at once, and once it’s in the policy, it comes out of the generator for free.
In practice, the change reads like the two halves of this article. Before, “build the charge endpoint” produced the happy path. Now the same six words produce the key column, the unique constraint, the stored response, and the duplicate test, because the requirement moved out of my head and into the repo.
Making the whole team inherit it
A skill in my home directory fixes my sessions, and nobody else’s, which would make this a productivity trick instead of an engineering control. The point is to take the requirement out of every individual’s working memory, so distribution is most of the value.
The cheapest move is putting the skill in the repository itself. Project skills live under .claude/skills/, which means they ride along with git. A teammate clones the repo and the policy is already in force in their sessions; nobody installs anything, and a new hire inherits it on day one with the rest of the codebase. Changes go through pull requests like any other code, so when someone improves the policy, the review happens in the open and everyone gets the improvement on their next pull. The skill becomes part of the codebase's definition of done rather than one person's habit.
Plugins cover the org where the repo stops. We run more than one service, and copying a policy file between repositories is how policies drift. Claude Code plugins bundle skills into an installable package, so a platform or security team can publish the company’s conventions once, idempotent writes alongside whatever else belongs in the standard kit, and every repo that installs the plugin gets the same versioned set. Updating the policy means bumping the plugin, not chasing down copies.
None of it holds without a backstop. Trust but verify. The skill instructs the model; it doesn’t bind the human who deletes the test or the contractor using a different tool entirely. So the same policy gets a deterministic check in CI, one that flags side-effecting routes with no idempotency-key handling and requires a concurrent-duplicate test on payment paths. The skill front-loads the right behavior, and the pipeline catches whatever slipped past it. Between the two, the requirement holds whether or not anyone remembered it that day, which was the entire problem.
There’s a quieter benefit I didn’t expect. Writing the skill forced the team to argue about the policy once, explicitly, instead of re-litigating it in fragments across a dozen code reviews. The document that came out of that argument is short, versioned, and binding on the tooling. Most teams never write their idempotency requirements down anywhere, and ours ended up as a file the tooling actually reads.
Where the list lives now
I still read every endpoint that changes something against the same short list: the client sending the request twice, two copies arriving in the same instant, the process dying halfway through, and whether a retry gets the first response back or a fresh error. What changed is where the list lives. It used to sit in my head and made it into maybe four prompts out of five. Now it sits in the repo, fires on every matching task for everyone, and CI catches the cases where it didn’t take. The judgment that comes from having been burned is still required, every bit of it. Writing it down turned out to be the only way it travels past the person holding it.
Idempotency keys are a small pattern. A header, a row, a unique constraint, a stored response. They were worth an article because they sit right on the line the model can’t reason across on its own, and worth a skill because the cost of relying on memory is a customer charged twice on the day somebody forgot to mention it.
By Joshua McDonald on June 11, 2026.
Exported from Medium on August 26, 2026.
Reader discussion