Key takeaways
- Strict idempotency keys prevent duplicate side effects regardless of LLM behavior.
- Transactional outboxes ensure atomic persistence before network transmission.
- Probabilistic retries create unreconstructable financial liability in production.
- Client-generated keys are required to survive payload mutations by the model.
The demo worked perfectly. The agent processed the refund, the UI updated, and the stakeholders nodded. Then production launched. The logs showed duplicate refunds firing whenever the LLM hit a transient network timeout. The model retried the call, assuming the first attempt failed. The backend, unaware of the duplicate intent, executed the refund again. This silent duplication erodes trust. It creates immediate financial liability that no dashboard alert catches in time.
You face a specific build decision now. Do you enforce strict idempotency keys and transactional outbox patterns for every agent tool invocation? Or do you rely on probabilistic LLM retries and hope for the best? This choice determines if your system treats side effects as atomic units or as best-effort guesses.
The desired outcome is zero duplicate side effects during transient network failures. The actual outcome is often double-charged customers or corrupted inventory records. The agent believes it succeeded when it did not. The core failure is that LLMs treat tool calls as stateless suggestions rather than committed transactions. When a timeout occurs, the model retries without checking if the first attempt actually persisted.
How do you distinguish a network drop from a server-side success?
Timeout ambiguity is the primary failure mode. The agent sends a request. The connection drops. The agent does not know if the server processed the request before the drop. If you let the LLM decide, it will likely retry. If the server had already processed the first request, you have a duplicate.
You must tie every tool invocation to a unique, client-generated idempotency key. This key is generated before the network call. It is stored in a durable outbox. If the agent retries, the backend recognizes the duplicate key. It returns the original result instead of executing again. This makes side effects deterministic regardless of agent behavior.
The key must be stable. It cannot be derived from the LLM's generated JSON, which might vary slightly on retry. It must be derived from the logical intent, such as the run ID and the specific action type. This ensures that even if the payload mutates, the identity remains constant.
What breaks when the LLM mutates the payload?
Payload mutation is a subtle killer. The LLM generates JSON for the tool call. On retry, it might add a field, change the order, or tweak a string format. If your deduplication logic hashes the entire payload, these variations create new hashes. The backend sees a new request. It executes again.
Hash-based deduplication fails here. You need semantic deduplication based on the idempotency key, not the payload content. The key is the anchor. The payload is the data. If the key matches, the payload is irrelevant to the execution decision. The backend returns the cached response associated with that key.
This requires a shift in how you design tool interfaces. The tool definition must explicitly accept an idempotency key parameter. The agent framework must inject this key before serialization. If the LLM tries to generate the key itself, you lose control. The key must be generated by the deterministic layer of your system, not the probabilistic layer.
Why does partial execution leave orphaned records?
Multi-step tools are dangerous. An agent might call a tool that creates a database record, then sends an email, then updates a cache. If the email step fails, the record exists. The agent retries the whole tool. The record is created again. You now have two records and one email.
This is partial execution. You need compensating transactions. If a multi-step tool fails, the system must roll back the completed steps. The idempotency key tracks the logical unit of work. If the key is seen again, the backend checks the state of the previous attempt. If it was partial, it triggers the rollback logic before accepting the new attempt.
Without this, you accumulate orphaned data. Cleanup scripts become necessary. These scripts are fragile and often run late. The financial impact is delayed but real. You are paying for data integrity issues that should have been prevented at write time.
How do race conditions create duplicate actions?
Two concurrent agent instances might issue the same logical action. One instance times out. The other instance is still processing. The first instance retries. Now two requests are in flight. They have different generated keys if the keys are not shared.
If the keys are generated locally per instance, you have a race condition. Both instances write to the outbox. Both send requests to the backend. The backend sees two different keys. It executes twice. The solution is a shared key generation layer. The key must be derived from a global source, such as a database sequence or a distributed lock.
This requires coordination. The agent framework must ensure that only one instance owns a specific logical action. If two instances detect the same intent, one must wait or abort. This adds complexity but prevents the most common source of duplicates.
What happens when the model hallucinates success?
The LLM might fabricate a confirmation response. It sees a timeout and assumes the tool succeeded. It tells the user the refund is complete. The backend never received the request. Or the backend received it but failed silently. The user believes the action is done. It is not.
This is hallucinated success. The agent's internal state diverges from the external world. You cannot rely on the LLM's interpretation of the tool response. You must verify the state externally. The idempotency key allows you to query the backend for the actual status. If the key exists in the outbox but not in the success log, you know the action is pending or failed.
This verification step is critical. The agent must poll or wait for a confirmation signal from the backend, not just the LLM's internal reasoning. The backend must expose a status endpoint that accepts the idempotency key. This closes the loop between intent and reality.
Loading diagram…
When should you defer strict idempotency?
You should not defer it for any tool with financial or data integrity implications. Payments, inventory, user data, and external API calls with side effects require strict idempotency. For read-only tools, the risk is lower. A duplicate read is harmless.
However, even read-only tools can cause issues if they trigger side effects indirectly. For example, a read tool that updates a cache. If the cache is corrupted, downstream logic fails. Assess the blast radius. If the tool writes anywhere, treat it as stateful. Apply the idempotency pattern.
The cost of implementation is low. The cost of failure is high. Do not defer based on complexity. Defer based on risk. If the tool is experimental and has no production traffic, you can skip it. But as soon as it touches real data, the pattern is mandatory.
How does this affect operator load and trust?
Operators hate debugging duplicates. They spend hours tracing logs to find why a customer was charged twice. They write scripts to fix the data. This is low-value work. It erodes trust in the system.
With strict idempotency, operators see clean logs. Every action has a unique key. They can trace the lifecycle of a single action from intent to completion. If a failure occurs, the state is clear. No guessing. No duplicate records to reconcile.
This reduces incident cost. Time-to-halt is faster because the root cause is obvious. Release confidence increases because you know the system will not corrupt data on retry. The operator load shifts from cleanup to monitoring. They watch the outbox for stuck keys. They verify that the backend is responding correctly. The work is proactive, not reactive.
Why is the transactional outbox the correct pattern?
The outbox pattern ensures atomicity. The agent writes the intent to the outbox table in the same transaction as the initial state change. If the transaction fails, the intent is not recorded. If it succeeds, the intent is durable.
A background worker picks up the outbox entry and sends the request to the tool API. If the send fails, the worker retries. The idempotency key ensures that the backend does not execute twice. This decouples the agent's logic from the network's reliability. The agent does not need to handle retries. The worker does.
This pattern is well-understood in distributed systems. It is not new. But it is often ignored in AI agent systems because the LLM layer is seen as separate. It is not. The LLM is just another component that generates intent. The intent must be treated with the same rigor as any other system event.
How do you build this capability step by step?
Diagnose the current failure modes. Look at your logs. Find the duplicates. Identify which tools are causing them. Model the logical units of work. What is the smallest atomic action? Build the key generation layer. Ensure it is deterministic. Harden the backend to accept and check keys.
This is a practitioner method. It is not a pitch. It is a way to think about the problem. Start with the most critical tool. Implement the pattern. Measure the impact. Then expand. Do not try to fix all tools at once. Focus on the ones with financial or data integrity risks.
The build is iterative. You will find edge cases. The LLM might generate keys in unexpected ways. The backend might have bugs in the key check logic. You will fix these. The system becomes more reliable with each iteration. The goal is not perfection. The goal is predictability.
What should you do this week?
Pick one tool that has caused a duplicate issue. Implement the idempotency key pattern for that tool only. Generate the key client-side. Store it in the outbox. Update the backend to check the key. Test the retry scenario. Verify that the duplicate is prevented.
This is your proof. It is a small change. It has a large impact. If it works, you have a template. You can apply it to other tools. If it fails, you have learned why. You have data to make a better decision.
Do not wait for the next incident. The cost of waiting is real. The first duplicate charge will happen. The first corrupted record will happen. You can prevent it. You have the tools. The decision is yours. Build the key. Store the outbox. Protect the data.
FAQ
- Why can't we just check the database before retrying?
- Race conditions occur between check and execution. Two concurrent agents may both see no record and both write, causing duplicates. A pre-generated key in the outbox prevents this.
- Does this add latency to every agent action?
- Minimal. Writing to a local outbox table is faster than a network call. The cost is one extra write, not a synchronous remote check.
- How do we handle multi-step tool failures?
- Use compensating transactions. If step two fails, step one must be rolled back. The idempotency key tracks the logical unit, not just the single HTTP request.
