One Timeout, Two Orders: How to Make AI Actions Safe to Retry
By Greg Nowak. Updated 25 August 2026.
An AI agent submits an order. The supplier accepts it, but the response disappears during a network timeout. The agent sees a failed call and tries again. Unless the integration recognises both attempts as one business action, the customer may receive two orders.
This is not limited to autonomous agents. The same failure can duplicate invoices, CRM records, support tickets, emails and webhook-driven jobs. A timeout means only that the caller did not receive an answer in time. It does not tell you whether the action happened.
Decide what a retry is allowed to repeat
Retries are useful for temporary network failures and overloaded services. Backoff, jitter and attempt limits control when requests are repeated, but they do not prevent repeated business effects. That protection must be part of each tool’s contract.
| Operation | Safe retry behaviour | Minimum safeguard |
|---|---|---|
| Read product or account data | Repeat after a temporary failure | Backoff, jitter and an attempt limit |
| Create or update a record | Return the first resource or result | Stable idempotency key and stored status |
| Place an order or send a message | Never repeat an accepted action | Provider retry key, plus reconciliation |
| Receive a webhook | Acknowledge a previously claimed event | Atomic event claim and idempotent worker |
| Start slow work | Report that the original job is still running | Durable queue, job ID and status endpoint |
Give one business intention one stable key
An idempotency key identifies the intended action, not an individual HTTP attempt. Create it when the workflow decides to perform the action, save it with the workflow state and reuse it for every retry. Generating a new UUID inside the retry loop defeats the protection because every attempt then appears unrelated.
Keep the material request unchanged. If the amount, recipient or product changes, create a new intention and therefore a new key. Conversely, reject a reused key whose request fingerprint differs from the original.
Provider behaviour varies. Stripe’s API v1 documentation says that it stores the first status and response body for a key and compares later parameters with the original request. LINE’s messaging guidance requires the retry key on the first attempt and blocks a later request after the original has been accepted. Both also impose retention rules, so your own ledger may need to remember an intention for longer than the provider does.
Make the ledger durable and the claim atomic
A useful action ledger records the key, operation type, request hash, status, timestamps, attempt count, provider request ID and completed result. The database—not process memory—should decide which worker owns the action.
For PostgreSQL, a small claim can use a primary key and ON CONFLICT DO NOTHING:
INSERT INTO action_ledger
(idempotency_key, operation, request_hash, status)
VALUES
(:key, :operation, :request_hash, 'processing')
ON CONFLICT (idempotency_key) DO NOTHING;If the insert succeeds, that worker owns the work. If it does not, read the existing row: return its stored result when completed, report that it is still processing, or apply an explicit recovery policy. A separate “check, then insert” sequence is unsafe because two workers can pass the check simultaneously. PostgreSQL documents ON CONFLICT as the database-level alternative for handling concurrent uniqueness conflicts.
Store the first useful response or a stable resource identifier. “Already processed” is often too vague for an agent; returning the original order ID gives it a safe way to continue the workflow.
Know the gap a ledger cannot close
A local ledger alone cannot guarantee exactly-once execution against an unrelated external system. A worker can call the supplier successfully and crash before marking its ledger row complete. On recovery, the system still does not know whether to call again.
The strongest design passes the same key to a provider that supports idempotency. If that is impossible, use a unique business reference the provider can query, or introduce a reconciliation state such as outcome_unknown and stop automatic retries until the result is checked. For consequential actions, an honest uncertain status is safer than guessing.
Queues help throughput, not duplicate prevention
Inbound webhooks present the problem in reverse: providers may deliver the same event more than once. Verify the signature, atomically claim the event identity, enqueue the work and acknowledge promptly. The worker must remain idempotent because queue messages can also be retried.
Stripe’s current webhook guidance recommends recording processed event IDs, accounting for logically duplicated events and handling work asynchronously. The queue separates receipt from execution; the ledger prevents two deliveries from producing the same effect.
Test success followed by silence
The most revealing test is not “the API returned an error.” Simulate the downstream service accepting an order and then dropping the response. Allow the agent or worker to retry and verify that only one business action exists.
- Send two simultaneous requests with the same key.
- Reuse a key with a changed payload and confirm it is rejected.
- Crash after the external call but before the local completion update.
- Retry after the provider’s key-retention window.
- Trace workflow ID, action key, attempt, provider request ID and final status together.
Start with the actions that can cause damage
Inventory every agent tool, scheduled job and webhook. Mark reads, reversible writes and consequential actions separately. Then document who creates the key, where it is stored, how long it survives, what response is replayed and how an uncertain outcome is reconciled.
Greg can help your team map these failure boundaries and build a reusable gateway for stable keys, atomic claims, result replay and operational logging. If retries currently depend on hope or manual cleanup, get in touch for a practical review.
Retries should repeat delivery attempts, not business decisions. With that distinction enforced, a timeout remains an operational nuisance instead of quietly becoming a second order.
Related on GrN.dk
- A Voice Agent Is Only Ready When the Human Handoff Works
- Agent-ready APIs: audit the contract before MCP rollout
- When AI writes JSON, one bad field can break the workflow
Need help with this kind of work?
Make your AI integrations safe to retry Get in touch with Greg.