TUTORIALS 10 min read

LLM Tool-Call Idempotency Keys: Prevent Duplicate Side Effects in AI Agents

Retries make AI agents reliable until a repeated tool call charges twice, posts twice, or creates duplicate records. Idempotency keys turn retries into safe replays.

By EgoistAI ·
LLM Tool-Call Idempotency Keys: Prevent Duplicate Side Effects in AI Agents

An agent times out after asking a payment tool to create an invoice. It cannot tell whether the remote service completed the request, so it tries again. The retry succeeds—and the customer now has two invoices. The model did what most reliability playbooks recommend, but the surrounding system made a repeated call unsafe.

LLM tool-call idempotency keys give retries a stable identity. The first accepted execution records the result; later calls with the same key return that result or report the original state instead of creating another side effect. This is a systems control, not a prompt instruction. Telling a model “do not run twice” cannot solve an ambiguous network outcome.

Identify which tool calls need protection

Read-only calls such as fetching a document are usually safe to repeat, although they can still be expensive. Mutating calls deserve stronger treatment:

  • charging a card or issuing a refund;
  • sending email or posting a message;
  • creating a ticket, order, deployment, or database row;
  • reserving inventory;
  • changing permissions;
  • triggering a long-running external job.

Delete operations can also be idempotent when deleting an already-absent resource returns success, but the semantics must be explicit. “Create a new campaign” is not naturally idempotent. “Ensure campaign cmp_123 exists with this specification” can be.

The agent runtime should classify tools by effect. Do not rely on the model to infer whether a call is dangerous from its name.

Derive a stable key from the logical action

Generate the key before the first attempt and preserve it across retries, process restarts, model fallbacks, and human-approved resumptions. A random UUID works if it is stored durably with the task. A deterministic digest can help when the logical operation already has a stable identity.

type ToolIntent = {
  runId: string;
  stepId: string;
  tool: string;
  normalizedArgs: unknown;
};

function makeIdempotencyKey(intent: ToolIntent) {
  return sha256(canonicalJson({
    runId: intent.runId,
    stepId: intent.stepId,
    tool: intent.tool,
    args: intent.normalizedArgs
  }));
}

Do not create a fresh key on every HTTP attempt. That defeats the control. Do not use only the raw prompt either: two intentional purchases could share text, while one logical purchase could be expressed in different words.

Normalize arguments before hashing. Sort object keys, resolve defaults, standardize currency units, and exclude volatile fields such as attempt number. Include tenant and authorization scope so keys cannot collide across customers.

Persist an execution record before the side effect

The tool gateway needs a durable table keyed by tenant, tool, and idempotency key. Store an argument digest, state, lease owner, timestamps, external correlation ID, and serialized result.

create table tool_executions (
  tenant_id text not null,
  tool_name text not null,
  idem_key text not null,
  args_hash text not null,
  status text not null,
  external_id text,
  result_json jsonb,
  updated_at timestamptz not null,
  primary key (tenant_id, tool_name, idem_key)
);

Use a unique insert to elect the first executor. A duplicate request must compare the stored argument digest. If the key is reused with different arguments, return a conflict instead of guessing which request the caller meant.

Statuses typically include started, succeeded, failed_retryable, and failed_terminal. A started record that never changes needs a lease or reconciliation process; otherwise a crashed worker can block the action forever.

Put the key at the strongest enforcement layer

If the downstream provider supports idempotency, forward the key. Stripe, for example, stores the result associated with an idempotency key for eligible POST requests. Provider enforcement protects the narrowest point where duplicates matter.

You still need a gateway record. It lets the agent recover the external identifier, enforce cross-provider semantics, and distinguish a completed request from a local timeout. For systems without native support, create a client-supplied resource ID or maintain the deduplication record in the same transaction as the local state change.

A database insert and an external API call cannot usually be one atomic transaction. Use an outbox or state machine: commit the intent locally, execute externally, then reconcile using a stable external reference. Never mark success before the provider confirms the operation.

Handle ambiguous outcomes explicitly

Timeout does not mean failure. It means the caller does not know the outcome. The retry path should first query the execution record and, where possible, the provider using its correlation ID.

async function executeOnce(req: ToolRequest) {
  const record = await reserve(req);
  if (record.status === "succeeded") return record.result;
  if (record.argsHash !== hashArgs(req.args)) throw new KeyConflict();

  const response = await provider.create(req.args, {
    idempotencyKey: req.idempotencyKey
  });
  return await markSucceeded(record, response.id, response);
}

If the provider accepted the call but the connection failed before returning an ID, retry with the same provider key. If native deduplication is unavailable, send the operation to reconciliation rather than blindly issuing it again.

Connect approval to the logical action

Human approval should authorize an intent, not a single network attempt. Store the approved argument digest with the idempotency key. A retry with identical arguments can reuse the approval; a material argument change requires a new review.

This prevents a model from obtaining approval for a $10 refund and then reusing the same step identity for $100. The gateway, not the prompt, compares canonical arguments.

Test the failure windows

Happy-path tests prove little. Inject failure after each boundary:

  1. after reserving the key but before calling the provider;
  2. after the provider commits but before the response arrives;
  3. after the response arrives but before local success is stored;
  4. during model or worker restart;
  5. when two workers race on the same key;
  6. when the same key arrives with different arguments.

Assert that the provider observes one logical mutation and every caller eventually receives the same terminal result. Track key conflicts, replay hits, stale started records, and reconciliation age in production.

Common mistakes

Short deduplication windows are dangerous when jobs can resume days later. Storing only success results leaves a failed-but-committed ambiguity. Putting the key in free-form model output invites formatting drift. Treating a tool-call ID generated by the model as globally unique confuses one sampled response with one business operation.

The key should live in orchestrator state and be opaque to ordinary prompt content. Logs may include a safe prefix for correlation, but avoid exposing tenant data encoded in a deterministic key.

The takeaway

Retries are mandatory in distributed AI systems, and side effects make retries dangerous. Give every mutating tool intent a durable idempotency key, enforce it in the gateway and provider, bind it to approved arguments, and test the ambiguous windows. Then an agent can retry aggressively without turning temporary uncertainty into permanent duplicate actions.

Share this article

> Want more like this?

Get the best AI insights delivered weekly.

By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.

> Related Articles

Tags

AI agentsidempotencytool callingdistributed systemsretriesAPI design

> Stay in the loop

Weekly AI tools & insights.