Email Idempotency: Stop Retries From Sending Duplicate Messages

Email Idempotency: Stop Retries From Sending Duplicate Messages

Design reliable email retries with stable notification identities, saved payloads, provider idempotency keys and honest handling of uncertain outcomes.

MailBlastr Team

TL;DR

  • Give each intended email a stable application-level identity and reuse its idempotency key when retrying the same request.
  • Store the request payload and provider receipt so a timeout does not force you to guess whether a send happened.
  • Treat a changed payload, a new notification and an intentional resend as separate decisions.
  • Provider deduplication has a retention window; keep your own durable send record for business-level protection.

A timeout does not prove the email was not sent

Duplicate emails often start with an understandable retry. Your application sends a request, the connection times out and a worker tries again. If the provider accepted the first request before the response was lost, a second unprotected request can create another message. Amazon SES documents this kind of ambiguity in its sending-process explanation.

The right question is therefore not simply “Did the HTTP call succeed?” It is “What happened to this intended notification?” Answering that requires a stable identity, a durable record and a retry policy that preserves both.

Define the notification before defining the key

Choose what counts as one business event. An order receipt, a shipping update and a password-reset request are different notifications even when they concern the same user. A key containing only the user ID would collapse unrelated messages.

For an illustrative order receipt, a logical identity might combine the order ID, recipient identity and notification purpose. Create that identity when the business event is committed, then store the rendered payload or the immutable inputs needed to reconstruct it.

Avoid putting a new random key inside every retry attempt. A random key can work if it is generated once and persisted with the logical send. Regenerating it after each failure removes the relationship between attempts.

Keep a durable application send record

A useful record can include the logical notification ID, recipient reference, template revision, payload digest, provider idempotency key, attempt state and provider message ID. Keep sensitive content out of routine logs; the operational record should be access-controlled.

Use a uniqueness constraint on the logical notification identity so two workers cannot independently create the same intended send. When a business transaction creates a notification, an outbox record committed with that transaction can help avoid a gap between saving the business change and queuing the email.

The worker should claim the record, send the saved payload and store the provider's result. A lease or lock helps coordinate workers, but it is not a substitute for the provider-side deduplication that protects an ambiguous external request.

Reuse the key only for the same request

MailBlastr's idempotency documentation covers the supported send endpoints, request matching and retention behavior. Its current send API reference documents the optional Idempotency-Key header and a 24-hour retention window. Check the current contract when implementing a client.

If an attempt times out, retry with the same key and the same payload within the supported window. Do not change the subject, recipient or body under that key and expect it to mean the same request. Treat a payload conflict as a programming or workflow issue that needs resolution, rather than automatically generating a new key and sending again.

An intentional resend should be explicit. For example, a user-requested replacement receipt can have a new logical resend event linked to the original notification. That preserves the difference between “retry the uncertain request” and “send another copy on purpose.”

Model uncertain outcomes honestly

Use states that distinguish an unsent notification, a request in progress, an accepted send and an unresolved outcome. A simple failure label can hide the very information the retry policy needs.

SituationUseful next step
Local validation failed before sendingCorrect the input; no provider request occurred
Provider returned a confirmed rejectionFollow the documented error behavior
Response was lost after dispatchReuse the same key and reconcile the saved request
Provider returned a message IDStore the receipt and follow delivery events
Deduplication window has expiredReconcile available evidence before deciding on another send

Do not promise exactly-once inbox delivery. Your application controls its logical requests; transport, mailbox handling and intentional resends are separate concerns.

Test the failure paths before production

Test two workers claiming the same logical notification, a lost response after provider acceptance, a repeated request with an altered payload and a retry after the retention window. Confirm that the application records the result without silently creating another notification.

Keep batch behavior separate from single-message behavior. A partial batch result can contain messages that were already sent. Follow the provider's documented receipt and retry rules rather than resending the entire recipient list blindly.

Connect sends to delivery evidence

Store the provider message ID with your logical notification and use verified delivery events to update the record. Webhook duplicates need their own deduplication; a send key does not solve event-processing duplication. The webhook retry guide covers that side of the workflow.

For a broader release check, use the transactional email testing checklist. A reliable send system should make each retry explainable from stored evidence, rather than depend on someone guessing whether an email probably went out.