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.
This isn't agent-specific — it's just newly relevant
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 →