Skip to main content
AI Agent Idempotency: Safe Tool Calls After Retries

AI Agent Idempotency: Safe Tool Calls After Retries

Best Practices
••5 min read•By Daily Miranda Pardo

A timeout. The agent waits. No response comes. The retry mechanism kicks in.

What the code doesn't know: the first call did reach the email server. The email already went out. The customer received it. When the retry executes the same tool, the customer gets a second identical email. Sometimes, two charges.

No exception in the logs. Everything looks fine.

This is the hidden cost of building AI agents in production without idempotency in tool calls.

Why Retries Break Agents Without Idempotency

When an agent calls a tool and gets no response, it cannot know what happened:

  • The call arrived, was processed, but the response was lost in transit
  • The call never arrived because the error occurred before sending
  • The call arrived but the server crashed before responding

Without idempotency, a retry duplicates any of these scenarios. Retries are necessary — and there's a detailed breakdown of exponential backoff retry and circuit breaker patterns worth reading. But if tools aren't idempotent, the very mechanism protecting your agent from interruptions can cause a bigger problem.

Read operations are naturally idempotent: you can run getCustomer() ten times and the result is always the same. The problem is writes with real-world side effects:

  • Sending a transactional email
  • Processing a payment through Stripe
  • Creating a database record
  • Sending a push notification
  • Posting a message to Slack or WhatsApp

Any of these can execute twice from a single timeout-and-retry cycle.

The Idempotency Key Pattern

The solution is the same one used by Stripe, Twilio, and every serious payments API: idempotency keys.

Before executing a tool, generate a unique key that identifies that specific operation in that specific run. If the key already exists in cache, return the stored result instead of re-executing:

import { createHash } from 'crypto';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

async function idempotentExecute<T>(
  key: string,
  operation: () => Promise<T>,
  ttlSeconds = 3600
): Promise<T> {
  const { data } = await supabase
    .from('idempotency_cache')
    .select('result')
    .eq('key', key)
    .gt('expires_at', new Date().toISOString())
    .maybeSingle();

  if (data?.result) {
    return JSON.parse(data.result) as T;
  }

  const result = await operation();

  const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
  await supabase.from('idempotency_cache').upsert({
    key,
    result: JSON.stringify(result),
    expires_at: expiresAt,
  });

  return result;
}

Generating the Right Key

The key must uniquely identify this operation in this run. The correct formula combines the run ID with a hash of the input:

import { createHash } from 'crypto';

function getIdempotencyKey(
  runId: string,
  toolName: string,
  input: object
): string {
  const inputHash = createHash('sha256')
    .update(JSON.stringify(input))
    .digest('hex')
    .slice(0, 16);

  return `${runId}:${toolName}:${inputHash}`;
}

If the agent sends the same email in two different runs (because the business logic requires it), those are two separate operations with two different runId values — both execute correctly. But if the same run calls send_email twice with the same input due to a retry, the second call finds the key in cache and returns the original result without re-executing.

Practical Implementation: Email Sending

interface SendEmailInput {
  to: string;
  subject: string;
  body: string;
}

const sendEmailTool = {
  name: 'send_email',
  description: 'Sends an email to the customer',

  async execute(
    input: SendEmailInput,
    runId: string
  ): Promise<{ messageId: string }> {
    const key = getIdempotencyKey(runId, 'send_email', input);

    return idempotentExecute(key, async () => {
      const messageId = await emailProvider.send(input);
      return { messageId };
    });
  },
};

The first execution sends the email and stores the messageId. If the timeout occurs after the email was sent but before the result reaches the agent, the retry queries the table and returns the original messageId — without sending a second email.

The Database Schema

create table idempotency_cache (
  key        text primary key,
  result     jsonb not null,
  created_at timestamptz default now(),
  expires_at timestamptz not null
);

create index idx_idempotency_expires
  on idempotency_cache(expires_at);

A cleanup job or Supabase function removes expired keys:

delete from idempotency_cache
where expires_at < now();

TTL by Tool Type

The TTL should exceed the maximum total run time plus retries:

Operation typeRecommended TTL
Payments, transactional emails24 hours
Notifications, record updates1 hour
Document generation, exports7 days
Read queriesnot applicable

When Idempotency Isn't Needed

The pattern is only meaningful for operations with real-world side effects:

  • Reads (getCustomer(), searchProducts()): naturally idempotent, no cache needed
  • Logs and telemetry: a duplicate is harmless
  • Analytical queries: no external state modified

Adding idempotency to reads consumes resources with no benefit.

Conclusion

An agent without idempotency in its write tools is an agent that will statistically duplicate a real-world action at some point. Not because the code fails — but because the world does. Timeouts, network drops, and server restarts are production events. The retry that prevents an interruption can cause a bigger problem if the tools aren't prepared for it.

If you already have retries configured in your agents, idempotent tool calls are the natural next step. Without it, every retry is a risk.

Building AI agents for production and need an architecture that survives timeouts and retries without duplicating actions? Message me on WhatsApp and we'll work through it together.

Share article

Repetitive processes in your business?

Download the free AI Automation Map — the 5 most time-consuming processes and how to fix them.

No spam. Just the PDF. Unsubscribe anytime.

Written by Daily Miranda Pardo

I help businesses automate processes, build AI agents and connect intelligent systems.