DEV Community

ZylahMorn61835
ZylahMorn61835

Posted on

Email and Phone Codes Under Load — Preserving Login Recovery and Account Continuity

Short answer: for an e-commerce login risk scorer, keep email and phone verification as separate, auditable recovery factors; choose the channel from the device-risk decision, and preserve an already verified factor until a deliberate recovery ceremony replaces it. Delivery speed is only one term in the bill. The expensive failure is an account that cannot be recovered or cannot be reconciled after a provider migration.

A device fingerprint is evidence, not identity. A new browser, a changed network, or a reset mobile identifier should raise a score, but it should not silently erase the account's recovery history. In payment and ledger work, I treat every verification attempt like a tiny financial event: it gets an idempotency key, an immutable audit record, and a clear expiry. That exactly-once mindset is useful here because duplicate sends and duplicate callbacks are normal, not exceptional.

What the delivery bill hides

The visible metric is delivery rate. The bill is made of more parts: messages sent, carrier or mailbox filtering, retries, support contacts, fraud review, and the retention cost of keeping evidence long enough to explain a decision. A phone code may arrive quickly but be unavailable after a number change. An email code may be delayed by filtering but remain reachable from a user's established mailbox. Neither channel is a universal fallback.

Measure twice.

Before moving off a managed provider, export a channel-level ledger for at least one complete business cycle. Count requested, accepted, delivered, verified, expired, and abandoned challenges, split by country, carrier or mail domain, device-risk band, and recovery outcome. For each cohort, join the challenge stream to support contacts and ledger reversals, inspect the lag between acceptance and verification, and sample the abandoned sessions to distinguish filtering from user confusion. Do not infer delivery from an HTTP 202 response. Record the provider message identifier and your own challenge identifier, then reconcile them asynchronously; retain the raw callback long enough to explain a dispute, while keeping its normalized status immutable.

Here is the minimum event shape I use. The hash is a reference to a normalized device signal, never the raw fingerprint; retention and privacy obligations still apply.

type VerificationEvent struct {
    ChallengeID   string    `json:"challenge_id\"`
    IdempotencyKey string    `json:"idempotency_key\"`
    AccountID      string    `json:"account_id\"`
    Channel        string    `json:"channel\"` // email or phone
    DeviceHash     string    `json:"device_hash\"`
    Status         string    `json:"status\"`
    OccurredAt     time.Time `json:"occurred_at\"`
}
Enter fullscreen mode Exit fullscreen mode

The retention decision is the uncomfortable part. Keeping every raw address, IP, and fingerprint makes investigations easier and privacy exposure larger. Keeping only a verdict makes dispute resolution weak. I retain the normalized evidence needed to reproduce the decision, encrypt contact identifiers, and attach a deletion deadline to each record. That is a trade-off, not a promise of perfect history.

How should email and phone verification handle delivery risk, recovery paths, and account continuity?

Start with a policy table, not a channel preference.

Situation Primary action Recovery gate Continuity record
Familiar device, low risk Avoid a new code Existing verified factor Last successful factor and timestamp
New device, medium risk Offer the channel with a healthy recent delivery rate One valid code plus rate limits Challenge and provider message IDs
High risk or changed contact Require step-up review Two independent signals or staffed review Reason code and reviewer/audit trail
Lost phone or mailbox Freeze sensitive changes Recovery ceremony with delay and notification Old factor remains marked, never deleted in place

A recovery path must be harder to abuse than the login path it protects. Rate-limit by account, destination, device cluster, and network; return the same user-facing response for unknown accounts; and make codes single-use with a short lifetime. OWASP's authentication guidance also treats recovery as an authentication function, so it deserves the same threat modeling as sign-in.

Account continuity depends on stable identifiers. Keep the account's verified-factor record separate from the current delivery provider. During migration, dual-write challenge metadata, send through one channel at a time, and compare outcomes before changing routing. A callback arriving twice must produce one state transition. A callback arriving after expiry must be recorded but cannot resurrect the challenge.

Pause.

I once assumed a 99% accepted-send rate meant the migration was safe. It did not: the missing one percent concentrated in high-risk new-device sessions, exactly where recovery mattered. The correction was to measure verified completion and successful recovery by risk band, not to chase a prettier aggregate. Your mileage may vary because mailbox and carrier behavior is regional, and I am not sure any single dashboard can expose that without joining support and ledger data.

The migration boundary is an accounting boundary

Treat the managed provider as an external journal. Define an internal state machine (created, accepted, delivered, verified, expired, rejected) and permit only monotonic transitions, with an explicit exception path for a human-reviewed recovery. Store provider status as an observation, not as authority over account ownership.

Run a shadow period in which the replacement path evaluates but does not send. Compare risk decisions, latency, duplicate suppression, and recovery completion. Then canary by geography and risk band. The rollback switch should change routing for new challenges while leaving already issued challenges verifiable, because invalidating them during an incident turns a delivery problem into an account-continuity outage.

Instrumentation should answer three questions within minutes: which channel is failing, which risk cohort is affected, and which accounts are accumulating retries. Alerts on send volume alone are noisy. Pair delivery signals with verification completion, support tickets, and reconciliation lag. For compliance, log who changed routing policy, when, and why; access to contact data should be separately auditable.

The catch is that this design is not suitable when you need anonymous, instant access with no durable account record. In that case, use a short-lived session or a platform-native passkey flow and accept a different recovery model. Stick with a managed provider when your team cannot operate regional deliverability, abuse controls, and 24-hour incident response; migration is a responsibility transfer, not a DNS change.

A small, testable verifier

The verifier below makes idempotency and expiry explicit. It deliberately accepts a generic sender interface, so the authentication policy remains portable across providers.

type Sender interface {
    Send(ctx context.Context, channel, destination, code, key string) (string, error)
}

func VerifyCode(now time.Time, event VerificationEvent, supplied string, expectedHash string) (VerificationEvent, bool) {
    if event.Status != "accepted" || now.After(event.OccurredAt.Add(5*time.Minute)) {
        event.Status = "expired"
        return event, false
    }
    if !subtle.ConstantTimeCompare([]byte(hashCode(supplied)), []byte(expectedHash)) {
        return event, false
    }
    event.Status = "verified"
    return event, true
}
Enter fullscreen mode Exit fullscreen mode

Test the awkward paths: duplicate requests with one idempotency key, reordered callbacks, expired codes, a changed phone number during a pending challenge, and a provider outage during recovery. Include property tests for monotonic state transitions. A green unit suite cannot prove mailbox placement, so keep a small regional synthetic test and reconcile it with production evidence.

References

Further reading

Top comments (0)