# Retrying Agent Tool Calls Without Doing Things Twice

Retries make agents resilient and, without idempotency, dangerous. How to design tool calls that are safe to repeat.

Published 2026-10-26. https://falnoa.com/en/blog/retrying-agent-tool-calls-without-doing-things-twice

Every agent platform ends up with retries. A model call times out, a tool returns a 502, a worker is restarted halfway through a run. The obvious fix is to try again, and for reads that is fine. For writes it is where the trouble starts.

An agent that sends an email, creates a ticket, issues a refund or updates a customer record is performing a side effect. If the first attempt actually succeeded and only the response was lost, the retry performs it twice. Nobody notices in a demo. In production it shows up as a customer who received the same message three times, or a refund that was paid twice.

This is an old problem in distributed systems. Agents make it worse in a few specific ways, and the old answer still works.

## Why agents duplicate more than ordinary services

A conventional service retries a request with the same payload. An agent often does not. When a tool call fails and the model sees an error, it may decide to "try again" with slightly different arguments, or with a different tool that does nearly the same thing. A retry at the infrastructure layer and a retry at the reasoning layer can stack on top of each other, and neither knows about the other.

There are three common sources of duplicate actions:

- **Transport retries.** The HTTP client or queue re-sends after a timeout, even though the server finished the work.
- **Run resumption.** A crashed or redeployed run restarts from its last checkpoint and replays steps that had already completed.
- **Model-level retries.** The model reads an ambiguous error, concludes the action failed, and issues it again.

Each needs a different defence, but they share one root cause: the system cannot tell "this is a repeat of the same intent" from "this is a new intent that looks similar".

## Give every intent an identity

The core idea is an idempotency key. The caller attaches a stable identifier to a unit of intent, and the receiving side guarantees that executing the same key twice has the same effect as executing it once.

For agents, the key should be derived from the run and the step, not generated fresh on every attempt. Something like the run id, the step index and a hash of the normalized arguments works. A transport retry, a resumed run and a replayed step all produce the same key. A genuinely new decision produces a different one.

Two details matter more than they look:

1. **Generate the key before the first attempt, and persist it.** If the key is created inside the retry loop, every attempt is a new intent.
2. **Store the result, not just the fact that the key was seen.** When a duplicate arrives, return the original response. An agent that gets "already processed" with no payload will often try to work around it.

Many payment and messaging APIs already accept an idempotency key. For internal tools and third-party systems that do not, the platform can enforce it in a thin layer in front of them: a table of keys, their status and their recorded results, written in the same transaction as the side effect where possible.

## Separate "decide" from "do"

A useful structural habit is to split every write tool into two phases. The agent records an intent ("send this message to this recipient, key K"), and a separate executor performs it. The intent is stored durably before anything happens. The executor claims it, performs the action, and records the outcome.

This gives you several things for free. A crashed executor can resume safely, because the intent record says what was attempted. A human reviewer can see exactly what is about to happen, which pairs well with approval gates. And the audit trail shows intent, attempts and outcome as separate facts instead of one ambiguous log line.

It also changes what the model sees. Instead of a raw tool error, the agent gets a status: queued, completed, failed with a reason, or unknown. Which brings us to the hardest case.

## Treat "unknown" as a first-class outcome

The dangerous state is not failure. It is not knowing. A request timed out and you cannot tell whether the action happened.

Most systems collapse this into "failed" and retry, which is exactly how duplicates occur. A better design has three outcomes: succeeded, failed, and unknown. For unknown, the executor first checks the target system, for example by looking up the record by key, before deciding anything. Only if that check proves the action did not happen does it retry. If no check is possible, the safe default for a consequential action is to stop and ask a person, not to guess.

The agent should be told the truth in the same terms. "Outcome unknown, verification pending" is something a model can reason about. A generic error invites a blind retry.

## Make the retry budget explicit

Retries should be bounded and visible, at every layer. Decide where each kind of retry lives and put it in one place:

- Transport retries belong to the executor, with backoff and a hard cap.
- Run-level recovery belongs to the orchestrator, which replays from a checkpoint and relies on keys to skip completed work.
- Model-level retries should be limited by policy: how many times may the agent reattempt a write, and what must it report when it gives up?

Log each attempt against the same key. When something does go wrong, you want to answer "how many times did we try to do this one thing, and why" with a single query.

## What to test

Idempotency is easy to claim and easy to break. Test it directly:

- Kill the worker between the side effect and the recorded result, resume, and assert that the effect happened once.
- Replay a completed run from its start and assert that no write is repeated.
- Return a timeout from a fake downstream that actually succeeded, and assert that the system verifies before retrying.

These are cheap tests with a deterministic fake, and they catch the failures that otherwise surface as angry customers.

## Our view

Reliability for agents is mostly ordinary distributed-systems discipline applied to a component that makes decisions at runtime. The model is not going to learn to be careful about duplicates on its own. The platform has to make repeating an action harmless, record what was intended, and be honest about what it does not know.

If your agents write to systems that matter and you want a second pair of eyes on how they retry, [get in touch](https://www.falnoa.com/#contact).
