4 API Retry Mistakes That Cause Duplicate Transactional Emails
Transactional email APIs are designed to be reliable, but reliability breaks when retry logic is implemented incorrectly on the client side. A 200 response that arrives late, a timeout that hides a successful send, or a network hiccup between your server and the ESP can all trigger a retry that delivers a second copy of a password reset or a receipt to a customer. These four mistakes explain many duplicate-send incidents.
last updated 2026-07-124 sections
section 01
Retrying on Timeout Without an Idempotency Key
When a send call times out, the request may have already reached the ESP and been processed. Without an idempotency key, a retry is treated as a new send. Many transactional email APIs accept an idempotency key header or parameter. Pass a stable key derived from the message context, such as a hash of user ID plus event type plus a short timestamp window, and the API returns the original response instead of sending again.
section 02
Treating 5xx Responses as Safe to Retry Immediately
A 500 or 503 from an ESP does not always mean the request failed. Some server errors occur after the message has been queued internally. Retrying immediately on any 5xx response without checking whether the first send landed creates duplicates. Implement exponential backoff starting at two seconds, and check your ESP delivery log before retrying any send that returned a 5xx status.
section 03
Not Deduplicating at the Application Layer
Idempotency keys are one layer of protection. A second layer is tracking send attempts in your own database before making the API call. Store a record of sent transactional emails keyed to the triggering event. Before any send, query that table. If a record exists and is recent, skip the send. This approach survives network failures between your app and the ESP and works regardless of whether the ESP supports idempotency keys.
section 04
Retrying Across Multiple Worker Processes Without Coordination
In distributed systems, the same job can be picked up by two workers if a lock expires before the first worker confirms completion. If the job includes a transactional send, both workers will attempt the send. Use distributed locks or exactly-once job semantics at the queue level, not just at the API call level. Configure lock expiry to exceed your maximum ESP response time with margin, so a slow but successful response does not release the lock early.
reading this as teams choosing a transactional API
Judged purely as an API. What the request contract guarantees on retry, how complete the webhook event coverage is, whether suppressions are readable and writable programmatically, and how much the message log retains when something needs debugging in production.
Applied to 4 api retry mistakes that cause duplicate transactional emails, that means weighing idempotency keys, webhook coverage, event stream, and operating track record ahead of the rest, against high-frequency programmatic sends where a missed delivery is a product failure.