October 10, 2026 · 7 min read
Idempotency for AI agents: duplicates, retries and quarantine
By Sahan, co-founder, systems and delivery
An action is idempotent when running it twice has the same effect as running it once. For an AI agent, that means every side effect (a posted task, a sent message, a published page, a ledger entry) carries a key, the first result is stored, and a retry replays that result instead of repeating the action.
This matters more for agents than for an ordinary API client, because agents retry on their own and get re-run by people. This post is how our own fleet handles it: 41 agents registered, 9 live, 5,450 runs and 0 failures in the 13 days to 2026-07-04, as shown on our agent fleet proof page. Each agent is a tomte, our word for one production agent with one defined job.
Why do agents duplicate side effects more than ordinary software?
Five things can repeat an action. The model retries a failed tool call. The scheduler fires at least once, and sometimes twice in one slot. Two entry points post the same record. A crash lands after the action but before the “done” mark. A person re-runs a job that already ran.
The tool protocol does not rescue you. The Model Context Protocol specification (2025-06-18) defines tool annotations: idempotentHint, destructiveHint, readOnlyHint and openWorldHint. It also says clients must treat annotations as untrusted unless the server is trusted. A hint is a label, not a guarantee. The side that does the work has to enforce the rule.
OWASP’s Top 10 for Agentic Applications 2026, published 9 December 2025, lists tool misuse (ASI02) and cascading failures (ASI08). A duplicate side effect can feed either one.
What are the four layers we run?
We run a stable key, a lock on the slot, a ceiling on retries and a quarantine for anything that should stop. Each answers a different question about the second arrival of the same work.
| Layer | Question it answers | What it does on the second arrival | Example in our fleet |
|---|---|---|---|
| Natural key + upsert | Is this the same record? | Updates the existing record, no twin | Our task backbone: one task per source and record |
| Slot lock | Is someone already doing this job? | Refuses, and the caller stops | Our content pipeline: one publish per site, date and slot |
| Retry ceiling | Have we tried this too often? | Stops, opens one human task, returns a “do not retry” code | The shared client behind every agent call |
| Quarantine hold | Should this wait for a person? | Parks the item with a written reason | Stalled work routed to its owner |
Layer 1: a stable key per record, and upsert instead of insert
The key answers “is this the same record?”, and the write is an upsert. In our task backbone, every task carries a source plus a stable per-record key. A unique rule on that pair means a repeat arrival updates the existing task rather than creating a twin.
Our first key was wrong. We built it from record id, owner and action, so a change of owner changed the key and left a stale duplicate. The key must be stable for the underlying record, not its current state. The wider design is in our post on an AI task management system for teams.
Create-only integrations left approval items open forever. The fix was an upsert on every stage change, plus a daily job that re-posts an item as completed once its condition clears.
Our ad-spend truth engine re-pulls platform data by a composite natural key (date, level, entity, platform), so a re-pull overwrites rather than appends. Lead ingest keys on the source’s own id.
Layer 2: a lock on the slot, taken before the side effect
A lock answers “is another run already doing this job?” In our content pipeline, every publishing run acquires a lock on site, date and slot first. A refusal means stop. The lock is released only once the publish is durably done.
Start and lock share one run key, so a late release from a zombie run cannot free the retry’s lock, and a superseded caller gets an explicit conflict, not a false success. A sweep releases a crashed run’s lock, and a time-to-live lets the next acquire take over a stale one.
We got this wrong twice. In August 2026 a double publish happened before the lock existed. In September 2026 a routine died holding a lock and blocked a daily slot. The fix is an automatic force-release that fires only when the holder is past its time-to-live, its run is confirmed finished, and no publish evidence exists for the slot. Each fire is audit-logged, with a kill switch.
A third failure was quieter. A guard allowing one published piece per topic per site sat inert for weeks, because no caller sent the topic key. A guard nobody calls is not a guard.
Layer 3: a ceiling on retries, and a code that means stop
A ceiling needs a stop signal the caller understands. One of our agents once retried a single rejected call over and over because nothing told it to quit.
Now, after a small fixed number of attempts on the same key, the system opens exactly one task for a human and returns a permanent-failure code. Our shared client treats that code as “do not retry”. Failed reports with blank error text are flagged, then rejected.
In our coding lane, automatic retry is off by default and limited to infrastructure causes: crashes, workspace errors, a verifier running out of turns. Failed gates, refuted verification, fenced paths, anything needing a secret, a merge or a spend decision, and any empty or unrecognised reason are never retried. Unknown cause means no.
Brakes sit around it: caps on re-queues and per-run volume, a cool-down and an age limit. The counter is written before the status flips, so a crash can only over-count, and a person’s concurrent edit wins. More patterns are in our post on AI agent failure modes in production.
Layer 4: quarantine, with the reason written down
Parked work always records a machine-readable hold reason. A hold with no reason is just a lost item.
Items with no state change for days are classed frozen and go to their human owner or a repair story, never back into a silent retry. A quiet scheduled agent with nothing to fix gets switched off, and that write is idempotent, since setting the same value twice is a no-op. Humans resume by clearing the hold.
Scheduled sweeps claim a lease keyed on job name plus hour bucket, so two fires in one hour do one pass. Apps write an intent row first and a drainer posts it with a stable key. An outbox with no drainer is a queue to nowhere.
One gap is open, and we would rather say so. Two entry points into a point-of-sale system could double-post a sale, and no reconciliation key exists there yet.
What do the vendors do, and what should you copy?
The big platforms agree on the shape and differ on the window. All four pages below were read in October 2026.
- Stripe’s API docs replay the saved response when a request repeats with the same key, including 500 errors. They recommend V4 UUID keys, error if the same key arrives with different parameters, and remove keys after at least 24 hours.
- AWS Powertools for Lambda stores key, status, expiry, in-progress expiry and a payload hash. Records expire after one hour by default, and a concurrent duplicate is blocked while the first call is in progress.
- Amazon SQS FIFO queues deduplicate a retried send within a 5-minute window, by content hash or explicit deduplication id.
- Temporal’s docs recommend idempotent activities, define that as repeats not changing state beyond the first, and note the key is enforced by the service you call.
Takeaways: the window is a design decision. The key must come from the caller. Concurrent duplicates need an in-progress state, not just a stored result.
What five questions should you ask about any agent that acts?
These are the five we check at handover.
- Does every side effect carry a key the caller chose? A downstream key changes on retry.
- What happens when the same key arrives with different content? The right answer is an error, not a silent overwrite.
- How long is the window? Past it, a repeat counts as new work.
- What is the stop code, and who reads it? A ceiling nobody reads is a log line.
- Where does a quarantined item go, and who clears it? Name the owner and the reason.
A buyer who cannot get written answers to these should not go live.
Where this fits
Idempotency is built into every agent an AI Workforce Sprint delivers: from $9,500, 4 to 6 weeks, with keys, locks and hold reasons in the handover documentation. Two weeks of written post-handover support are included, and you own the delivered code, data and documentation. Fixed price, fixed scope.
Start async
Send us the list of actions your agents take that cost money or send a message to a person. We reply in writing within one business day with which ones need a key, a lock or a ceiling. No meetings. Start async.