Idempotency Keys Aren't Optional for AI Agents That Take Actions

Any tool call an agent makes that has a real-world side effect — a charge, a message send, a booking, a database write — needs an idempotency key generated once per logical action and reused on every retry of that action, checked server-side before the effect runs a second time. Without it, the exact failure mode that makes retries safe for read-only calls (timeout, retry, no harm done) becomes the failure mode that duplicates a charge or sends the same WhatsApp message twice.
A tool call that only reads data is safe to retry without thinking about it — call it twice, get the same answer twice, no harm done. The moment a tool call does something — charges a card, sends a message, books a slot, writes a row that triggers a downstream workflow — retrying it blindly stops being safe, and "the model called this tool" is not a good enough boundary to have skipped that distinction.
Where this actually bites
It rarely shows up as a dramatic failure. It shows up as: a network blip between your agent and a payment provider times out after the charge actually went through, the agent's retry logic (or the orchestration framework's default retry-on-timeout) calls the same tool again, and now there are two charges for one transaction. Or an outbound WhatsApp message send times out server-side after delivery, gets retried, and a customer gets the same message twice — annoying for a marketing message, actively confusing for a "here's your order confirmation" message with different order details each time it fires.
The fix: an idempotency key per logical action
Every side-effecting tool call needs a key that identifies the logical action being performed, generated once when the action is first attempted, and reused on every subsequent retry of that same logical action — not a new key per HTTP request. The server receiving that call checks whether it has already processed that key; if so, it returns the original result instead of performing the effect again.
idempotency_key = f"charge:{order_id}:{attempt_group_id}"
result = payment_client.charge(
amount=amount,
idempotency_key=idempotency_key, # reused across retries of THIS charge
)Where the key lives matters
The key has to be generated at the point where the logical action is first decided on — typically when the agent's plan commits to "charge this order" — not inside the retry loop itself, or every retry generates a fresh key and defeats the whole point. For agent frameworks that don't give you an obvious hook for this, the practical pattern is deriving the key deterministically from stable inputs to the action (order ID plus action type plus a coarse time bucket), so two calls that represent the same real-world action always produce the same key even if they originate from different code paths.
Keys need a lifetime, not just uniqueness
An idempotency key that's remembered forever eventually becomes a problem in the other direction — a genuinely new action that happens to derive the same key (a customer placing an identical order a year later) gets silently treated as a duplicate of the old one. A bounded retention window — long enough to cover any realistic retry delay, short enough that a legitimately repeated action later isn't falsely suppressed — is the actual design decision, not "store every key forever." What that window should be depends entirely on the action's own realistic retry timeframe.
This is the same discipline that matters for AI systems built for low-connectivity markets, where retried and out-of-order deliveries are the normal case, not the exception. For the cost side of unbounded retries specifically, see the LLM cost curve nobody models.
Frequently asked questions
How long should an idempotency key be retained?
Long enough to cover the realistic maximum delay between an action and its last possible retry — commonly hours to a few days depending on the system — but not indefinitely, since an unbounded retention window risks incorrectly treating a genuinely new, later action as a duplicate of an old one.
Do read-only tool calls ever need idempotency keys?
Generally no — a pure read has no side effect to duplicate, so retrying it freely is safe by definition. The moment a tool call writes, charges, sends, or triggers any downstream effect, it needs one; the read/write distinction is the actual test, not whether the call happens to be made by an agent.
The takeaway
Idempotency keys are old, well-understood infrastructure — Stripe has supported them for years for exactly this reason. What's new is how easily an agent framework's default "retry the tool call on timeout or error" behavior reintroduces the problem for teams who never had to think about it before, because their previous integrations were request-response calls a human triggered once. An agent making autonomous decisions about when to retry needs the same discipline a payments engineer has had for a decade, applied to every tool that isn't purely read-only.

Have a similar challenge?
Book a free 30-minute architecture call and we'll tell you honestly whether and how we can help.
Book Free Discovery Call →