Canonical: https://botbento.com/blog/agent-tool-retry-idempotency-keys/
Format: Markdown representation of the public HTML page.

[Home](/) / [Blog](/blog/)

FIELD NOTES / 4 MIN READ

# How Do You Stop a Retried Tool Call From Running Twice?

When a bot's tool call times out, the side effect may already have happened even though no response arrived. MCP's idempotentHint labels a tool as safe to repeat, but it's an unverified hint, not a mechanism. An idempotency key -- generated once and reused on every retry -- is what actually lets a server replay a result instead of duplicating a write.

By BotBento Editorial · Published 2026-09-24 · Updated 2026-09-24

AI-assisted editorial: researched and drafted with AI using the primary sources linked below. Examples are illustrative; they are not measured product results. [Editorial policy](/editorial/).

## In this article

- [Why a timeout is not the same as a failure](#the-retry-problem)
- [What MCP's idempotentHint actually promises](#idempotenthint-is-a-hint)
- [The mechanism that actually prevents duplication](#what-an-idempotency-key-actually-does)
- [A worked example: a support-ticket tool](#worked-example)
- [What to check before you let a bot retry a tool call](#what-to-check-before-you-ship-a-retry)[Read as Markdown ](/text/blog/agent-tool-retry-idempotency-keys/index.md)

## Key takeaways

- A timeout is an ambiguous outcome, not a failure: the tool's side effect may have already run even though the response never arrived at the client.
- MCP's idempotentHint tells a client a tool is safe to call again with the same arguments, but the spec treats it as unverified metadata, not a guarantee.
- The actual protection is an idempotency key generated once per operation and reused on every retry, so the server can return the stored result instead of repeating the write.

## Why a timeout is not the same as a failure

A bot calls a tool. The network stalls, or the server is slow, and no response comes back before the client gives up. The natural move is to retry. But a timeout only tells you that a response didn't arrive in time -- it tells you nothing about whether the request was received and acted on.

If the tool created a support ticket, sent a payment, or wrote a row to a database, that side effect may have already completed on the server before the connection dropped. A blind retry then repeats the write: two tickets, two charges, two rows. The bot's own logs will show one call, but the system it touched will show two.

This is the core problem any agent that calls external tools has to solve before it retries anything: the client and the server can disagree about what happened, and the client is the one deciding whether to try again.

## What MCP's idempotentHint actually promises

The Model Context Protocol lets a server describe a tool with annotations, including idempotentHint, which signals that calling the tool repeatedly with the same arguments has no additional effect beyond the first successful call. On the surface this looks like exactly the guarantee a retrying client needs.

It isn't. The specification is explicit that these annotations are hints, not enforced behavior, and clients should not rely on them without additional verification, especially when the server is untrusted. A tool author can mark a tool idempotentHint: true and still implement it in a way that creates a duplicate record on a second call -- the annotation describes intent, not a contract the runtime checks.

For a bot developer this matters because it shifts the responsibility back onto the client and the tool's actual implementation. Reading idempotentHint on a tool definition tells you what the server author believes about their own tool. It does not tell you what will actually happen if your agent calls it twice after a timeout.

Sources: [Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools).

## The mechanism that actually prevents duplication

The pattern that provides a real guarantee, used across payment and infrastructure APIs, is an idempotency key. The client generates a unique value once, before the first attempt, and sends that same value with every retry of the same logical operation. The server stores the key alongside the result of the first successful execution. If a request arrives with a key it has already processed, the server returns the stored result instead of running the operation again.

This works because the server -- not the client -- is the one deciding whether an operation is new or a repeat. The client's job is only to keep reusing the same key across retries of the same intended action, and to generate a fresh key for a genuinely new action.

HTTP itself distinguishes methods that are safe to retry from those that aren't: GET is defined as safe and idempotent by nature, while POST is defined as neither, which is exactly why POST-style tool calls -- creating a ticket, charging a card, sending a message -- are the ones that need an explicit idempotency key rather than relying on the method itself.

None of this is something a client can verify from outside. Whether a tool honors an idempotency key, and whether it stores keys long enough to matter for your retry window, are implementation details of the server. If the tool's documentation doesn't mention idempotency keys, assume retries can duplicate the effect.

Sources: [Idempotent requests](https://docs.stripe.com/api/idempotent_requests), [HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110).

## A worked example: a support-ticket tool

Illustrative example. A personal bot has a tool called create\_ticket that takes a title and description and files a support ticket. The bot calls it, the connection times out after 8 seconds with no response, and the bot's retry logic fires.

Without an idempotency key: the bot calls create\_ticket again with the same title and description. If the first call actually succeeded server-side before the timeout, there are now two tickets. The bot has no way to tell, from its own side, that this happened -- both calls looked identical to it.

With an idempotency key: before the first call, the bot generates a key, for example a random string tied to this specific ticket-filing intent, and sends it as part of the request. On retry, it sends the exact same key. If the server implements idempotency keys, it recognizes the second request as a repeat of the first and returns the original ticket ID instead of filing a new one. The bot ends up with one ticket either way.

The difference between these two outcomes is entirely in whether the tool's server side was built to store and check keys -- the bot's retry code looks almost the same in both cases.

## What to check before you let a bot retry a tool call

Before adding automatic retries to any tool call that writes, sends, or charges something, check these points rather than assuming idempotentHint or a timeout policy covers you.

- Does the tool's documentation mention an idempotency key or a similar request-deduplication parameter? If not, treat retries as unsafe by default.
- If idempotentHint is set on an MCP tool, confirm what backs it -- a documented key mechanism, or just the author's description of intended behavior.
- For any tool that has a side effect outside the conversation (a write, a send, a charge), generate one key per intended action and reuse it across every retry of that action, never generating a new key just because the previous attempt timed out.
- Log the key alongside the action so a human reviewing the bot's run can tell a genuine repeat attempt from two separate, intended actions.
- If the tool gives no way to pass a key, prefer surfacing the timeout to a human before retrying, rather than guessing.

## Primary sources

Sources checked 2026-09-24. Standards and product documentation can change; follow the linked version when implementing.

- [Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) — Model Context Protocol
- [Idempotent requests](https://docs.stripe.com/api/idempotent_requests) — Stripe
- [HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110) — IETF

BotBento is in development. [Suggest a correction](/contact/).
