The login alert fires first. A student says the verification code never arrived, the support queue starts filling, and the delivery dashboard still reports an accepted message. The least complex fix is to treat SMS OTP as a multi-stage delivery system, with sender identity, carrier policy, route choice, and fraud controls measured separately.
Short answer: SMS OTP delivery fails because an accepted API request is not proof of handset delivery. US and EU carriers filter traffic, require different sender registration, and may reject or delay shared routes when the pattern resembles fraud. Build explicit status telemetry, register senders before launch, keep messages transactional, and provide a second factor when carrier delivery is outside your control.
What does the on-call actually see when an OTP disappears?
Usually, two clocks disagree. The application records send() as successful, while the carrier later returns a filtered, expired, or unknown status. A retry can then produce two valid codes, and the slower one wins in the student's inbox. That is a security and support problem, not just a messaging problem.
One receipt can lie.
Work backwards from the page. Correlate the login attempt, OTP hash, provider message ID, destination country, sender identity, and delivery receipt. Keep the code itself out of logs. A useful event model has requested, submitted, delivered, undelivered, expired, and verified; “submitted” is an intermediate state, not a success metric.
I keep the verification record idempotent: one login challenge has one active code and one expiry, while sends carry an idempotency key. That makes a queue replay safe. It also makes the postmortem legible: we can tell a carrier rejection from our own duplicate enqueue. I've been paged for missed jobs and duplicate deliveries, and the fastest way to lose an hour is to debug the provider dashboard before checking whether our worker submitted the same challenge twice.
Why can carrier filtering and sender registration break US/EU OTP routes?
Carriers apply local rules to sender identity and traffic type. In the United States, application-to-person traffic may require A2P 10DLC registration; Twilio's compliance documentation describes campaign and brand information as part of that process. European routes vary by country and operator. Alphanumeric sender IDs can be restricted or replaced, and some markets require pre-registration or a local originator.
Shared routes add another variable. Multiple customers can appear behind the same originator or route, so a carrier's anti-fraud model may score the aggregate pattern rather than your single tenant. Bursts of identical six-digit messages, rapid sends to many countries, mismatched country codes, and links with poor reputation all look different from a normal account-signup flow.
The operational response is boring and effective: maintain a country matrix, record which sender type is permitted, and test with real numbers on each target carrier. Do not assume that a green provider response means the handset saw the message. Your mileage may vary by operator and by policy revision; the carrier documentation and your delivery receipts are the evidence to trust.
A small Go boundary that keeps delivery states honest
The application should depend on a narrow interface. Provider-specific status names stay at the edge, where they can be mapped to the state model above.
package otp
import (
"context"
"time"
)
type SendRequest struct {
ChallengeID string
To string
Body string
Idempotency string
}
type DeliveryState string
const (
Submitted DeliveryState = "submitted"
Delivered DeliveryState = "delivered"
Undelivered DeliveryState = "undelivered"
)
type Sender interface {
Send(context.Context, SendRequest) (messageID string, err error)
}
type Receipt struct {
MessageID string
State DeliveryState
At time.Time
}
The worker writes submitted only after it has a provider message ID. A receipt handler advances the state, and a scheduled expiry closes challenges that never reach the handset. Verification accepts the latest unexpired code once, then marks the challenge consumed. That sequence prevents a retry storm from becoming a login bypass.
How should teams choose routes, fallbacks, and anti-fraud limits?
Start with the user journey, not a vendor feature list. If an edtech signup serves a few countries, dedicated sender registration and a single well-observed route may be easier to operate than automatic failover. If coverage is broad, a routing layer can select a country-approved sender and preserve the same message contract.
Fallbacks need a policy. A voice call or authenticator enrollment helps a student who cannot receive SMS, but silently sending the same OTP through two routes can create duplicate-code confusion. Rate-limit by account, device, IP, and destination; add a cooldown after repeated failures; and keep fraud rules independent from carrier delivery so a blocked attempt does not trigger endless retries.
The catch is that SMS is a reachability channel, not a guaranteed security channel. It is not suitable as the only factor for high-value administrator actions or recovery from a compromised phone number. Stick with an authenticator app, passkey, or hardware-backed factor when phishing resistance matters more than signup reach.
| Decision | Prefer | Trade-off |
|---|---|---|
| Sender ownership | Registered, dedicated originator | More setup and country-specific paperwork |
| Route selection | Explicit country rules | Less automatic coverage |
| Retry behavior | One active challenge, idempotent sends | A user may wait for the next allowed attempt |
| Fallback factor | Passkey or authenticator | More enrollment friction |
Alert on the gap between submitted and delivered, segmented by country, carrier, sender type, and template version. Track verification completion and time-to-verify, not just provider acceptance. Sample message bodies only in a redacted test environment; production telemetry should carry hashes and IDs.
For release checks, exercise a registered sender on a small set of US and EU test numbers, then replay provider callbacks out of order. A good test proves that an old receipt cannot revive an expired challenge, and that a duplicate queue message does not issue a second valid code. I am not sure any dashboard can expose every carrier decision in real time, so keep a support path that can capture the destination operator and timestamp from the user. The runbook should also record the template revision, the sender registration record, and the exact callback sequence; when a carrier changes a filter, those three details are usually what separates a useful escalation from a vague “SMS is slow” ticket.
False positives have a cost. A threshold that pages on every delayed receipt trains the team to ignore the alert; one that waits for a weekly trend leaves a signup outage running. Set a small warning window for one country or carrier, then page on a sustained verification-rate drop with enough volume to mean something.
Measure the journey, not the button click.
References
- https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- https://www.twilio.com/docs/messaging/compliance/a2p-10dlc
Top comments (0)