Software retries work because networks are unreliable. A request times out, a worker restarts, or a response disappears before the application records it. Trying again is often the correct engineering decision.
Email creates a special problem: the first request may have succeeded even when the application never received the confirmation. A blind retry can send the same receipt, password link, alert, or welcome message twice.
The customer sees duplication. The system sees two successful requests.
Transactional email needs a retry design that separates “try the request again” from “send another message.”
Understand the ambiguous failure
Imagine an application sends a receipt through an email API. The provider accepts the message and returns success, but the network connection drops before the application receives the response.
The application cannot tell whether the provider accepted the request. If it retries with no shared identity, the provider may treat the second request as a new email.
This ambiguity appears during timeouts, gateway errors, process crashes, queue redelivery, and manual replay. It cannot be solved by assuming every timeout means nothing happened.
Give the logical email a stable identity
Create an idempotency key for the business event, not for the individual network attempt.
A receipt could use an order identifier and message type. A password reset could use the reset request identifier. A trial reminder could use the account, stage, and scheduled date.
Every retry of the same logical email must reuse the same key. A genuinely new email must receive a new key.
Avoid randomizing the key inside the retry loop. That produces a unique request each time and defeats the protection.
Define what “the same email” means
The rule should match the customer experience.
Two attempts are the same when they represent one event and should produce one message. Two order receipts for two orders are different. A corrected invoice may be different from the original. A second password reset request may replace the first or create a new message depending on the security model.
Document the event boundary before implementation. If the team cannot explain when a new key is created, duplicate prevention will become inconsistent across services.
Store the provider result
When the first request succeeds, retain the email identifier and status associated with the idempotency key. A later retry should return the existing result instead of creating another send.
The application can then record the provider ID, correlate delivery events, and continue the workflow without guessing.
Choose an appropriate retention period. A receipt key may need to remain valid for a long time. A short-lived notification may need only enough coverage for queue retries and operational replay.
Make the queue idempotent too
API protection is one layer. The job system should also understand duplicates.
Queues commonly provide at-least-once delivery, which means a worker may receive the same job more than once. Store the event state before acknowledging the job. Use a stable job identifier and make repeated processing safe.
Do not mark the email as sent before the provider accepts it. Do not send first and record later without a recovery strategy. The workflow should survive a crash between those steps.
Keep personalization deterministic
A retry should not silently produce different content under the same key.
Capture the template version, recipient, subject variables, locale, and important links used for the logical message. If those inputs change, decide whether the system should return the existing send or create a new event with a new identity.
This is especially important for expiring links and security messages. Generating a new token during each retry can create several valid links even when only one email is delivered.
Test the failures that create duplicates
Happy-path testing will not reveal retry problems. Build a small failure matrix:
- accept the email, then drop the API response;
- time out the client before the provider responds;
- deliver the same queue job twice;
- restart the worker after the provider accepts the send;
- retry manually from an operations screen;
- submit the same key with different content;
- send a genuinely new event for the same recipient.
Verify that the logical email arrives once, every attempt is observable, and a new business event still produces a new message.
Connect delivery events to the original business event
Delivery, bounce, open, click, and reply events should map back to the same email identity. This makes support and debugging easier.
When a customer says they received two receipts, the team should be able to see whether two business events occurred, one event was retried incorrectly, or the mailbox displayed a thread in an unexpected way.
Signed webhooks and stored provider identifiers help preserve that chain of evidence.
SendHustle provides an email API for transactional messages as well as campaigns and automations. It supports idempotent sends so the same request can be retried without emailing twice, reusable templates, scoped API keys, and signed webhooks for delivery and engagement events.
Retries are a reliability feature only when the customer receives one logical message.
Explore SendHustle at https://sendhustle.com/.
Top comments (0)