Idempotent tool calls: do this before you let an agent retry

June 24, 20265 min readBy Harman Kamboj
AIAgentsIdempotencyBackend

An agent that can retry is an agent that can do the same thing twice. I started caring about idempotent tool calls the day I watched a sensible little agent send a message, sit there waiting for a reply that never came, decide the call had failed, and send the exact same thing again. The person on the other end got two of everything. The agent was sure it had acted once. If you are wiring an agent up to tools that touch real state, this is the bug that waits for you in production instead of in the demo.

I have spent years on the kind of systems where running an action twice is not a cosmetic slip. A cross-chain bridge that releases funds twice. A swap that submits the same transaction again. A job queue that double-charges a card. So when agents showed up calling tools in a loop, the failure mode looked very familiar, just wearing a new outfit.

Retries are going to happen whether you plan for them or not

People talk about agent retries like a feature you switch on. They are not. They leak in from every direction. The network times out. The orchestrator restarts a failed run from the top. The model itself, unsure whether its last call landed, decides to call the same tool one more time to be safe. You do not get to opt out of retries. You only get to decide whether they are safe.

The real gap is between "it worked" and "the agent heard back"

Here is the part that trips people up. A tool can do its job perfectly and the agent can still believe it failed. The write commits to the database, the email leaves the server, and then the connection drops before the response makes it back. From the agent's side that looks identical to a real failure. So it does the only reasonable thing it knows: it tries again. The work was already done. Now it is done twice.

You cannot close that gap by making the network reliable, because you do not control the network. You close it at the tool, by making a second identical call cost nothing.

Make the tool idempotent, not the agent careful

The tempting fix is to make the model smarter about when to retry. Resist that. The model is the worst possible place to put a correctness guarantee, because it is a probabilistic thing and your money is not. Put the guarantee in the tool, where it is plain code you can test.

The pattern is old and it comes straight from API design. The agent attaches an idempotency key to every call that changes state. The key is derived deterministically, usually a hash of the turn id, the tool name, and the arguments, so a genuine retry produces the same key and a brand new action produces a new one. The tool server keeps a small record of keys it has already handled. If it sees a key again, it skips the work and returns the result it gave the first time. Stripe has shipped exactly this for over a decade, and it is the same instinct I leaned on when I wrote about background jobs and queues, where a worker that picks up the same message twice has to land in the same place either way.

Give it a retry budget so a loop cannot bankrupt you

Idempotency keeps a retry from corrupting data. It does nothing to stop a retry from running forever. I have seen a single misbehaving tool drag an agent into a tight loop, calling, timing out, calling again, burning thousands of tokens before the outer timeout finally woke up and killed it. So every turn gets a retry budget, a hard cap on how many times a tool can be attempted before the agent has to stop and report that it is stuck. A loud failure is cheaper than a quiet one that bills you by the token.

Reads are cheap, writes are where you slow down

Not every tool needs this. A tool that only reads can be called ten times with no harm done, so do not wrap it in machinery it does not need. The discipline is to sort your tools into the ones that change something and the ones that do not, then spend your attention only on the first group. This is the same line I draw when trimming what an MCP server feeds an agent: most of the work is deciding what actually deserves the weight.

What I actually wire up

When I am setting an agent loose on real tools, the checklist is short and I do not skip it. Every tool that mutates state takes an idempotency key as a first-class argument. The server keeps a dedup table with a time-to-live, usually a day, so old keys do not pile up forever. The agent derives the key the same way every time so a retry collides on purpose. Each turn carries a retry budget. And I log the key on every call, because the first time you see two calls share a key you know a retry happened and you know your guard caught it.

None of this is clever. It is the boring reliability work that backend engineers have done for years, pointed at a new kind of caller that happens to be a language model with a habit of second-guessing itself. The teams that ship agents people trust are not the ones with the fanciest prompts. They are the ones who assumed the agent would do everything twice and made sure twice was safe.

If this is the kind of problem your product is fighting, I write more about it in my AI notes, and the rest of the work is on the homepage.

Building something where this matters?

I am open to senior full-stack, Web3, or AI engineering roles, fully remote and any timezone. If your agent or the hard part of your product is fighting you, that is the work I like.

Get in touch →