Somewhere in the stack, a retry met no idempotency contract.
A user gets two password resets, an agent reports confusion about “already sent,” and the question becomes: who re-sent it? Almost always, the answer is a retry — a timeout, a reconnecting network, or an agent deciding an uncertain response means failure. The fix is not “retry less.” It is an idempotency contract that makes retries safe.
Where duplicates come from
Sending email is a mutation over an unreliable network. Any client that cannot get a response — timeout, connection reset, process restart mid-request — faces a choice: give up, or retry without knowing whether the first attempt landed. Retrying without an idempotency contract is a coin flip: if the first request did land, the retry duplicates the message.
The dangerous case is silent: the first request succeeded server-side, the response was lost, and both attempts “look fine” in separate logs. Duplicates from this pattern cluster around deployments, flaky networks, and retry loops with backoff.
How Marmunk binds keys
Every send requires a stable idempotency key. Marmunk binds the key to the canonical send semantics — recipients, subject, content, sender, attachments — within one tenant and operation:
Identical retry → returns the original msg_* resource. The retry receives a new req_* (it is a new API attempt) but the same message. No second email exists. Changed semantics → 409 idempotency_conflict, loudly, rather than a quietly different email sent under the same key.
Why agents multiply this
LLM agents retry more than conventional code, for two reasons. First, they are often wrapped in generic retry harnesses — the same loop that fetches a web page five times will “helpfully” re-send an email five times. Second, an agent that sees a timeout reasons in natural language: “the request may have failed; let me try again” — with no key to make the try idempotent unless the tooling demands one.
Requiring an idempotency key on every send is deliberate friction here. The agent must derive the key from the business operation (“receipt for order 8291”), which is exactly the discipline that makes its retries harmless.
Using keys correctly
Derive the key from the business event, not the code path: receipt:v1:order_8291 — stable across retries, processes, and deploys. Version the key format so a legitimate re-send (a corrected receipt, a new campaign version) is a new key, not a conflict. Reuse the same key for the whole life of the business operation.
The full contract — key binding, conflict semantics, and worked examples — is in the Idempotency & retries guide.