Home / AI & GenAI

AI & GenAITroubleshootingAdvanced

AI agent tool calls executed twice: retries, timeouts and re-planning that send duplicate emails, tickets and payments

An agent creates two tickets, sends the same email twice or repeats a refund. Trace where the duplicate came from (HTTP retry, model re-planning, framework replay) and make side-effecting tools idempotent with keys, dedup stores and confirmation steps.

Deepak Bhardwaj5 min read

Applies toLLM agents with tool/function calling (OpenAI, Azure OpenAI, Bedrock, Anthropic APIs), agent frameworks and MCP servers that perform side effects

Symptoms

  • Duplicate tickets, emails, messages or records created by an agent, usually seconds apart.
  • More rarely: a payment, refund or account change applied twice.
  • Logs show one user request but two (or more) executions of the same tool.

Agents are built from several layers that each retry or repeat work. Without protection, a side-effecting tool runs once per repetition.

Where duplicates come from

Layer How it repeats a tool call
HTTP client to the tool API Request times out after the tool succeeded; client retries
LLM API call The model call returning the tool call is retried (timeout, 429, failover to another region); the new response contains a new, similar tool call
Agent loop / re-planning The tool result was unclear or slow; the model decides to call the tool again ("I'll try creating the ticket again")
Framework replay Durable workflow or checkpointed agent resumes after a crash and re-executes the last step
User Clicks retry or re-sends the message after a slow response
Queue delivery At-least-once message delivery runs the agent step twice

Step 1: trace one duplicate

For a duplicate pair, collect:

  • Tool call IDs from the model responses (tool_call_id / toolUseId).
  • Timestamps of each model call and tool execution.
  • Whether the two executions share a model response (same tool call ID) or come from two model turns.
Finding Source
Same tool call ID executed twice Your executor or framework replayed it
Different IDs, second after a tool timeout/error Model re-planned after an unclear result
Different IDs, two model calls for the same turn LLM API retry/failover
Two user messages User retry

Tracing (OpenTelemetry spans per model call and tool call) makes this a one-minute job instead of a log hunt.

Step 2: make side-effecting tools idempotent

The tool, not the model, must guarantee "at most once". Give every side-effecting operation an idempotency key derived from the business intent, not from the attempt.

import hashlib, json

def idempotency_key(conversation_id: str, intent: dict) -> str:
    # Same user turn + same normalised intent = same key, whatever the retries
    payload = json.dumps({"conv": conversation_id, **intent}, sort_keys=True)
    return hashlib.sha256(payload.encode()).hexdigest()

def create_ticket(conversation_id, title, body, requester):
    key = idempotency_key(conversation_id, {"op": "create_ticket", "title": title.strip().lower(), "requester": requester})
    existing = dedup_store.get(key)
    if existing:
        return {"status": "already_created", "ticket_id": existing}
    ticket_id = ticketing_api.create(title=title, body=body, requester=requester,
                                     headers={"Idempotency-Key": key})
    dedup_store.put(key, ticket_id, ttl_seconds=86400)
    return {"status": "created", "ticket_id": ticket_id}
  • Use the downstream API's own idempotency support where it exists (many payment and messaging APIs accept an Idempotency-Key header).
  • Store keys with a conditional write (INSERT ... ON CONFLICT DO NOTHING, DynamoDB attribute_not_exists) so two concurrent attempts can't both pass.
  • Return a clear result for the duplicate case ("already created, ticket 4711"). This also stops the model from trying again.

Step 3: give the model unambiguous tool results

Many re-plans happen because the tool returned something vague ("error", a timeout, an HTML page). Return structured, final outcomes:

{"status": "created", "ticket_id": "INC-4711", "note": "Do not create another ticket for this request."}

For timeouts, check the downstream state before reporting failure: "a ticket with this key exists" means success.

Step 4: separate deciding from doing for risky actions

Risk Pattern
Low (search, read) Let the agent call freely
Medium (ticket, email to internal staff) Idempotent tools + per-conversation limits ("max 1 ticket per turn")
High (payments, account changes, external emails) Two-step: the agent proposes an action; a human or a deterministic rule confirms; execution uses the confirmed action's ID as the idempotency key
proposal_id = store_proposal(action="refund", order_id=..., amount=...)
# UI shows proposal; user approves
execute_refund(proposal_id)   # executes once; repeated calls return the existing result

Step 5: framework and infrastructure settings

  • Disable automatic retries on tool execution in the agent framework for non-idempotent tools, or make them idempotent first.
  • Durable workflow engines replay steps by design; mark side-effecting steps as activities with idempotency keys.
  • Queue consumers: deduplicate on message ID before running the agent step.
  • LLM gateway retries: retrying the model call is fine as long as tools are idempotent; the risk is in what the new response triggers.

Caution

Never rely on prompt instructions ("never call create_ticket twice") as the protection. Models follow them most of the time, not all of the time. Enforce it in code.

Monitoring

  • Count tool executions per conversation turn by tool name; alert when side-effecting tools exceed 1.
  • Log idempotency-key hits; a rising hit rate shows retries or re-planning increasing.
  • Review duplicate incidents in a weekly agent quality review.

Prevent it next time

  • Classify every tool as read-only or side-effecting at design time.
  • Require idempotency keys for all side-effecting tools, enforced in the tool server (or MCP server), not in the agent.
  • Return explicit, final statuses from tools.
  • Add human confirmation for high-impact actions.