The bill for retrying a failed email API call includes submission attempts, worker capacity, retained evidence, and the investigation required when an outcome remains ambiguous. For a healthtech compliance notice, the dominant storage term can be repeated message bodies: under an illustrative workload of 10,000 notices, four retained attempt snapshots per notice, and a 100 KB body, those snapshots occupy 4 GB before indexes or replication. Keeping one body per notice reduces that component to 1 GB; attempt metadata remains additional. These are design assumptions, not measured traffic or prices.
Short answer: use capped exponential backoff with jitter for failures that the API contract identifies as transient, honor applicable retry instructions, and preserve a stable notice identity across attempts. Bound retries by both an attempt budget and a notice deadline. When submission may already have succeeded, reconcile the outcome or use documented idempotency support before sending again. Store compact evidence for each attempt instead of repeatedly storing the sensitive payload.
Evidence has costs.
The optimization is to retain the evidence needed to establish what happened while limiting duplicate work and duplicate content. An HTTP response accepting a request, a mail server accepting a message, and a person reading a compliance notice are different claims; an audit record should identify precisely which claim it supports. The central trade-off is storage reduction versus forensic detail: compact records cost less space, but cannot replace a deleted message body during an investigation.
What does another failed email API call actually cost?
Consider the notice as a ledger obligation: one authorized communication, associated with a particular recipient reference and notice version, with several possible transport attempts. This is a proposed data model, not a claim that an email transport can provide exactly-once delivery. The application can enforce uniqueness of its own obligation and state transitions; external submission still depends on the receiving system's documented behavior.
For planning, let N be the number of notices, A the average number of recorded attempts, P the payload size, and M the attempt metadata size. Retaining a full payload with every attempt requires approximately N × A × (P + M) bytes. Referencing one retained payload requires approximately N × P + N × A × M. The illustrative workload above removes 3 GB of duplicate body storage, but it does not reduce message submission fees, establish a retention period, or account for backups. It changes one specific term.
Retries multiply work.
That multiplication also affects incident response. If each attempt has an unrelated identity, an operator must reconstruct whether four submissions represent four notices or four attempts to satisfy one obligation. A stable notice ID, attempt number, submission timestamp, outcome category, and remote receipt reference make that distinction explicit. Keep recipient details behind a restricted reference when possible; an audit trail that reproduces medical content in every log entry creates another sensitive dataset to govern.
Which failures deserve exponential backoff?
Classification must precede scheduling. HTTP semantics in RFC 9110 distinguish an idempotent operation from an operation whose repetition may have additional effects, and caution against automatically retrying a non-idempotent request without a basis for knowing that repetition is safe. A timeout after transmitting a submission is therefore an unknown outcome: the sender lacks confirmation, but that absence does not establish rejection.
The following categories are an application policy to check against the actual email API contract. They are not a universal interpretation of every provider's status codes.
| Observed outcome | Proposed action | Evidence to retain |
|---|---|---|
| Explicit temporary rejection or documented throttling | Schedule a bounded retry; respect applicable server guidance | Classification, attempt count, next eligible time |
| Explicit permanent validation or authorization rejection | Stop automatic submission and route for correction | Sanitized reason and notice reference |
| Timeout or connection loss after possible submission | Mark the outcome unknown and reconcile | Stable idempotency key, time window, receipt if available |
| Confirmed API acceptance | Stop submission retries and track later delivery evidence separately | Receipt reference and acceptance timestamp |
A common trap is to treat every unsuccessful client call as proof that the message was never accepted. Backoff changes the timing of a duplicate; it does not prevent the duplicate. If an API documents idempotency, verify the key's scope, retention window, and payload matching rules before relying on it, and reuse the same key for the same intended submission. A locally generated key alone provides no remote guarantee.
Email delivery adds another boundary. RFC 5321 defines temporary and permanent SMTP reply categories, but the HTTP API may expose those later through its own event model. Avoid starting a second application submission loop merely because a downstream SMTP attempt was deferred; first determine which system owns that retry. Likewise, SPF concerns authorization to use a domain in an email identity. RFC 7208 is relevant to deliverability, but increasing a retry count does not repair an authorization configuration.
A bounded Go schedule with durable evidence
Use a delay window that grows exponentially until it reaches a cap, then sample jitter within that window so concurrent failures do not all receive the same scheduled timestamp. The example below proposes six total submission attempts, including the original, with retry windows of 2, 4, 8, 16, and 32 seconds. These values illustrate the mechanism; the API contract, notice deadline, and operational capacity must determine production values.
package notice import ( "math/rand" "time" ) // NextRetry returns a scheduling decision, not permission to resubmit. // completedAttempts includes the original submission attempt. // retryFloor is an already parsed, applicable server retry time. func NextRetry( now, deadline, retryFloor time.Time, completedAttempts int, rng *rand.Rand, ) (time.Time, bool) { const maxAttempts = 6 const base = 2 * time.Second const capDelay = 32 * time.Second if completedAttempts < 1 || completedAttempts >= maxAttempts || rng == nil { return time.Time{}, false } window := base for n := 1; n < completedAttempts && window < capDelay; n++ { window *= 2 if window > capDelay { window = capDelay } } // Sample uniformly from [0, window), then respect the server floor. delay := time.Duration(rng.Int63n(int64(window))) next := now.Add(delay) if next.Before(retryFloor) { next = retryFloor } if !next.Before(deadline) { return time.Time{}, false } return next, true } The caller must classify the failure, establish that resubmission is permitted, and parse any applicable Retry-After value before calling this function. RFC 9110 permits Retry-After to express either a date or a delay; convert it to a timestamp and preserve the raw value only if operationally necessary. The server floor may exceed the local backoff cap. If it falls beyond the notice deadline, escalate the obligation instead of shortening that floor to force another attempt.
This schedule has a limitation: it doesn't enforce a shared rate limit across workers. It is not suitable as the sole control for a fleet sharing one submission quota; add admission control at that shared boundary. Jitter spreads eligible times, but cannot guarantee that aggregate traffic stays within the quota.
Persist the chosen next eligible time with the attempt outcome. A worker restart should recover a pending obligation from durable state, rather than begin a fresh six-attempt budget. Use a conditional state transition or equivalent transaction to claim work, and record the attempt before crossing the network boundary. A crash between external acceptance and local confirmation still produces uncertainty; recovery must enter the reconciliation path. The exactly-once mindset is useful here because it forces every ambiguous boundary into view, even though the transport itself supplies no such guarantee.
One implementation detail matters: give each worker its own random source, or synchronize access to a shared one. Inject the clock and random source into tests. Neither timing nor chance should make a correctness assertion intermittent.
Test the ambiguity, then deploy the budget
The most useful test sends a request to a controlled endpoint that records acceptance and then withholds the response. Verify that the sender records an unknown outcome, preserves the notice identity, and follows its reconciliation policy. Also test an explicit temporary rejection, a retry instruction beyond the deadline, exhaustion after six attempts, duplicate delivery events, and a worker restart after a retry timestamp has been persisted. These are proposed acceptance tests; they are not reported production results.
Observe unresolved obligations.
Attempt counts alone can make an unhealthy system look busy. Track the age of the oldest unresolved notice, unknown submissions awaiting reconciliation, exhausted obligations, and the delay between local acceptance and whatever downstream evidence the contract actually provides. Define an operational escalation before releasing the worker, including who can authorize a fresh notice when the original outcome cannot be resolved. An alternative channel requires its own policy and evidence requirements.
For deployment, introduce the retry policy behind a controlled rollout and verify that restarting workers preserves both attempts consumed and next eligible timestamps. A larger retry budget increases queue occupancy and investigation volume, so evaluate it against unresolved obligations and duplicate risk. Raw throughput cannot establish that a compliance notice was delivered.
Retain the obligation and deliberately expire the payload
Separate notice content from attempt evidence. A proposed record can retain a notice reference, content version, restricted recipient reference, timestamps, outcome transitions, and receipt references, while a distinct store governs access to the body. If a digest is retained, it can support comparison with an available artifact; it cannot reconstruct deleted content, prove delivery, or establish what the recipient read. An append-only interface also needs access controls and integrity protections before its audit trail can be trusted.
No universal healthtech retention period follows from an HTTP or email standard. The responsible legal and privacy reviewers must establish the applicable obligations, including any hold that overrides routine deletion. They must also determine whether the available delivery evidence satisfies the particular notice requirement. Transport acceptance alone cannot settle that compliance question.
The deliberate stopping point is to stop keeping full response bodies, repeated message snapshots, and sensitive diagnostic fields once their approved operational purpose ends, while preserving the approved minimum evidence for the obligation. That choice has a concrete incident cost: after deletion, investigators may be unable to reconstruct the exact message or explain a provider-specific response from local records. Agree on that loss before enabling expiration. Reliable retrying requires both a bounded sending policy and an honest account of what the surviving record can prove.
References
The standards below define the protocol boundaries discussed here. The workload, retry limits, record design, and tests are explicit engineering proposals.
Further reading
- RFC 9110, HTTP Semantics: https://www.rfc-editor.org/rfc/rfc9110.html
- RFC 5321, Simple Mail Transfer Protocol: https://www.rfc-editor.org/rfc/rfc5321.html
- RFC 7208, Sender Policy Framework (SPF): https://datatracker.ietf.org/doc/html/rfc7208

