The bill for e-commerce signup verification and password reset email includes retained request keys, delivery attempts, event histories, and investigations into ambiguous timeouts. Duplicate sends after a retry multiply both email traffic and sensitive data kept for each reset link. If one signup creates one intent and three transport attempts, storing three complete copies of the recipient address and verification URL multiplies sensitive retained data without establishing which email arrived. Keep one short-lived intent, compact attempt records, and a separately governed audit trail. Treat a timed-out submission as an unknown outcome, never as proof that nothing was sent.
Short answer: Commit the token and notification intent together, deduplicate retries against the intent, and reconcile uncertain submissions before deciding whether to submit again. You can make local intent creation exactly once under a unique database constraint. You cannot infer exactly-once delivery to a mailbox from a network timeout. The same approach prevents multiple independent password reset links when a request is retried.
How can a password reset email retry avoid duplicate sends after a timeout?
An HTTP client deadline says the caller stopped waiting. It says nothing definitive about whether the remote service accepted the request, whether a recipient server accepted the message, or whether a human opened it. RFC 9110 discusses retries after connection failures; a mail submission has an external side effect, so the application needs an explicit retry policy. SMTP acceptance is a narrower fact than delivery to a person under RFC 5321.
For signup, use a stable intent identifier associated with an account and verification purpose. A second click while the first attempt is unresolved should refer to that intent. For password reset, the same principle applies, but the response must not reveal whether an address belongs to an account: OWASP's Forgot Password Cheat Sheet calls for consistent responses and single-use, expiring tokens. Make signup links single-use and expiring under a documented policy as well; the standards do not prescribe one universal lifetime.
The difficult interval is between successful remote acceptance and a lost acknowledgement. Imagine the worker submitting attempt 1, the transport accepting it, and the response timing out before the worker receives it. The local record now says unknown. A queue redelivery starts attempt 2 while the first message is still in transit; both can arrive, even if the worker logged only one acceptance. Blind retrying can thus produce two emails containing the same link. Rotating the token for every retry is worse: the first email can arrive after the second and lead to a broken link. An audit trail should distinguish "submission attempted, acknowledgement unknown" from "submission accepted"; neither means "inbox delivered." This is why an attempt counter is evidence of submissions initiated, not a count of messages received.
No acknowledgement, no certainty.
Bound the dominant retained term
Estimate storage from inputs before selecting a retention period. Let N be signup intents per day, A mean submission attempts per intent, B bytes of repeated payload and recipient data per attempt, and M bytes of compact attempt metadata. Full copies retained for D days cost approximately N × A × B × D bytes; a single encrypted intent payload plus compact attempts moves the repeating term toward N × A × M × D. This is a sizing equation, not a benchmark. Measure B and M on representative records, including indexes and backups, before assigning a capacity figure.
Three attempts for one intent should produce one encrypted recipient/link payload and three small attempt records, not three persisted link bodies. An attempt record needs an intent ID, attempt ID, timestamps, outcome category, and a correlation identifier if the transport supplies one. Restrict access to recipient addresses and token-bearing URLs. Store a hash of the verification token when validation can hash the presented token; avoid putting raw links in logs, traces, or event dumps. The alternative, storing a complete payload for each attempt, makes retrospective rendering easier but exposes multiple copies of the same secret across primary storage, backups, and any exported investigation dataset. Decide which evidence an investigator actually needs before retaining another copy.
One intent. Several attempts.
Do not confuse deduplication retention with audit retention. A unique key on account, purpose, and active generation prevents concurrent workers from minting independent links; it is an application invariant, not a historical record. Keep active token and deduplication state through the policy-defined period in which requests and links remain valid. Retain security evidence under an explicit organizational legal and operational schedule, with access controls and deletion rules. DKIM, defined in RFC 6376, authenticates a signed message domain and selected content; it cannot prove that a human received a message or that a worker submitted it only once.
How should a worker handle an unknown submission?
The transaction boundary is local: insert the token hash and notification intent with a unique key in the same database transaction. A worker leases the intent, records a unique attempt before calling the transport, and records acceptance only when it receives an acknowledgement. On timeout it records an unknown result, preserving the attempt's correlation identifier. Where a transport offers documented lookup or idempotency, reconcile using the original identifier after checking its guarantees and retention period. Without either capability, choose an explicit wait-and-resubmit policy and accept that duplicate delivery remains possible.
This Go sketch illustrates the decision boundary; database implementations must enforce uniqueness and atomic state transitions, rather than relying on process-local locks.
package verification
import (
"context"
"errors"
)
var ErrUncertain = errors.New("submission outcome unknown")
type Intent struct {
ID string
AttemptID string
State string
}
type Store interface {
LeasePending(context.Context) (Intent, error)
MarkAccepted(context.Context, string, string) error
MarkUnknown(context.Context, string, string) error
}
type Sender interface {
Submit(context.Context, string, string) error
}
func Process(ctx context.Context, db Store, mail Sender) error {
intent, err := db.LeasePending(ctx)
if err != nil { return err }
if intent.State != "pending" { return ErrUncertain }
err = mail.Submit(ctx, intent.ID, intent.AttemptID)
if err != nil {
if markErr := db.MarkUnknown(ctx, intent.ID, intent.AttemptID); markErr != nil {
return markErr
}
return ErrUncertain
}
return db.MarkAccepted(ctx, intent.ID, intent.AttemptID)
}
A failed local write after remote acceptance is another unknown outcome. A production worker must recover expired leases into reconciliation, including when the process terminates between submission and MarkAccepted. The sketch classifies every submit error conservatively as unknown; an adapter may distinguish an authoritative pre-submission rejection only if its contract guarantees that no submission occurred. Never let two leases submit the same unresolved intent concurrently.
Test the evidence, then delete deliberately
Exercise this boundary with a transport stub that accepts a submission but delays its response beyond the client deadline. Restart the worker before the acceptance write. Verify that one intent and one token hash survive, that the attempt is marked unknown or recoverable after lease expiry, and that the next worker does not automatically mint a new link. Test two simultaneous signup requests, a second request after token consumption, and an expired token. Count intents, accepted acknowledgements, unknown attempts, and link redemptions separately. A successful redemption is stronger evidence of use than an acceptance callback, but cannot prove that every earlier submission was unique.
Deployment deserves the same discipline: install the uniqueness constraint in the database migration before enabling multiple workers, and make lease recovery visible in monitoring. Alert on old unknown attempts and growing outstanding intent counts, not a fictitious "exactly once delivered" metric. For an e-commerce signup flow, correctness includes a customer who retries while mail is slow and an investigator who later needs to explain why two copies arrived.
Finally, stop keeping full message bodies, raw link URLs, and indefinite per-attempt payload copies solely for debugging. This reduces exposure and the storage term multiplied by attempts, but costs the ability to reconstruct an exact historical email after the active payload expires. Preserve minimal identifiers, timestamps, outcome categories, and controlled security evidence needed for investigation; document that limitation before an incident demands data deliberately deleted.
Top comments (0)