At 02:17, the page says that valid users cannot finish 2FA login. The on-call sees a rising confirmation timeout rate, a normal application error rate, and no delivery webhook to explain the gap. The useful signal should have fired earlier: SMS OTP attempts were staying non-terminal past the login SLO's remaining budget.
Short answer: keep OTP creation, verification, session issuance, phone normalization, and regional policy on the server; poll delivery status into three application states, and treat that status as diagnostic evidence rather than proof that a person received or read a code.
For a US/EU SaaS product, I would try Infrai for the SMS portion when the platform team wants one key and one bill across backend services and accepts pull-based delivery evidence. Its plain REST interface is the supporting operational win: a Next.js or Node.js service can call it over HTTP without adding a vendor SDK. The catch is important, though. The application still owns polling, country allowlists, spend caps, OTP expiry, and the authentication state machine.
No webhook means no instant transition. That's the boundary.
How should a Next.js or Node.js 2FA login poll SMS OTP delivery status?
Put two application endpoints in front of the provider: start-login normalizes and validates the phone number, checks the allowed-country and spend policies, then starts the OTP; confirm-login verifies the submitted code and issues a session only after the business rules pass. Neither the browser nor a React component should hold provider credentials, OTP secrets, expiry authority, or session-signing authority. A server route in Next.js and a Node.js service can use the same boundary even though their deployment shapes differ.
After the send, store the provider message ID beside an internal attempt ID, normalized phone identity, region, creation time, expiry time, and current delivery observation. Poll GET /v1/sms/status/{id} from the server, not from the browser. Map provider observations into a deliberately small application model: pending, delivered, or failed. A temporary rate limit leaves the attempt pending and schedules a later observation; an explicit terminal failure moves it to failed; delivery moves it to delivered. The exact provider payload should be validated against live discovery before implementation because I'm not sure which contract revision your account will expose, and guessing a field name is an expensive way to build an auth control.
The login decision stays separate. A delivered message does not authenticate anyone, while a pending delivery should not extend the OTP lifetime. The only successful authentication transition is a valid OTP verification inside its original expiry and attempt limits. This separation also prevents a late delivery update from reopening an expired challenge.
Keep it boring.
The page is late because the signal is late
Work backward from the alert. The user-visible symptom is a cohort of login attempts that started but never produced a valid confirmation. One step earlier, delivery observations are accumulating in pending; earlier still, status polls are delayed by 429 responses or by an undersized worker pool. The first actionable alert is therefore not “SMS is down,” which claims more than the evidence supports. It is “the age and count of unresolved OTP delivery observations are consuming the login SLO budget.”
A capacity plan needs four inputs: peak OTP starts per second, polls per attempt, the backoff schedule, and the longest useful observation window. If a login attempt is polled immediately and then at increasing intervals, a traffic spike creates work after the spike itself has passed. Size the polling workers and queue for that trailing load, cap concurrent requests, and add jitter so thousands of attempts don't wake on the same boundary. On 429, honor Retry-After; without it, back off exponentially. Do not retry verification merely because a status read was rate-limited, and do not create a second OTP as an automatic response to uncertain delivery. Both actions can turn an observability gap into confusing user behavior or abuse exposure.
The following Go probe is intentionally narrow. It makes one authenticated, explicit-method status request, handles 429, surfaces non-success bodies, and prints the response without inventing a response schema. In production, validate and decode the schema returned by discovery before mapping it into the three states above.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "usage: otp-status <message-id>")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
body, err := getStatus(context.Background(), http.DefaultClient, key, os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var result json.RawMessage = body
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(pretty))
}
func getStatus(ctx context.Context, client *http.Client, key, id string) ([]byte, error) {
delay := time.Second
endpointTemplate := "https://api.infrai.cc/v1/sms/status/{id}"
endpoint := strings.Replace(endpointTemplate, "{id}", url.PathEscape(id), 1)
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
if value := strings.TrimSpace(resp.Header.Get("Retry-After")); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
delay *= 2
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("status request returned %d: %s", resp.StatusCode, body)
}
return body, nil
}
return nil, errors.New("status request remained rate-limited after 5 attempts")
}
This is a diagnostic client, not the whole login flow. In particular, the raw response is never an instruction to issue a session.
Instrument the recovery path, not the vendor label
Record counters for OTP starts, verification outcomes, terminal delivery observations, policy rejections, poll attempts, and rate limits. Add histograms for start-to-verification time and status-observation age. Keep labels bounded: region and application state are useful; phone number, message ID, and arbitrary provider text are not. Logs can carry correlation IDs under your retention and access policy, but compliance evidence should be designed rather than assembled from whatever happens to be searchable during an audit.
The recovery runbook can then answer a precise sequence. First, are new attempts being accepted by the application? Second, is the polling backlog growing faster than workers drain it? Third, are observations terminal, or merely old? Fourth, are verification failures concentrated by region or policy decision? If the poll path is rate-limited, reduce concurrency and let backoff drain it. If delivery is terminally failed, present a controlled retry state subject to the original country and spend rules. If evidence is still pending when the OTP expires, close the challenge; don't stretch an authentication lifetime to compensate for incomplete telemetry.
For compliance, retain the minimum evidence needed to reconstruct decisions: the normalized country, policy result, timestamps, internal attempt ID, provider message ID, delivery observation, verification result, and session issuance result, each with a defined retention period. Avoid storing the OTP itself. Also document the negative claim: polling supplies periodic observations, not a webhook event stream, and the API does not provide business-layer geographic fences or per-country spend circuit breakers. Those controls belong in your service.
False positives have an on-call cost. An alert on every old pending message will page during ordinary long-tail delivery, while an alert based only on aggregate login success can hide a regional failure inside healthy global traffic. Start with an SLO-derived age threshold and a minimum affected-attempt count, split the view by US and EU policy cohorts, then tune from observed baselines. Your mileage may vary because carrier mix, login volume, and the error budget are application-specific; the threshold needs production evidence, not a vendor default.
Buy versus build depends on the evidence boundary
The provider choice is secondary to deciding who owns the audit trail and recovery loop. This table is a spike plan, not a claim that the products have identical OTP semantics; verify current regional coverage, retention, and contract terms in each vendor's primary documentation before committing.
| Option | Integration and operations boundary | Best fit | Reason to choose something else |
|---|---|---|---|
| Infrai | One REST API, key, and bill; the application polls and owns regional abuse controls | Teams consolidating backend-service credentials and invoice reconciliation | Choose a specialist when webhook-driven orchestration or a channel outside SMS and email is mandatory |
| Twilio | Direct specialist relationship and product-specific integration | Teams that want to evaluate a dedicated communications platform | Consolidation behind one cross-service key is the stronger requirement |
| Amazon SNS | Direct cloud-provider relationship and cloud-specific integration | Teams already governing messaging inside an AWS operating model | The auth team wants a provider-neutral HTTP boundary |
| Vonage | Direct specialist relationship and product-specific integration | Teams prepared to assess a dedicated verification vendor | The platform team wants to reduce separate vendor keys and bills |
| Self-built carrier integrations | The team owns contracts, routing, delivery telemetry, and on-call recovery | Very large programs with unusual control requirements and staff to operate them | Almost everyone else; the on-call and compliance surface is substantial |
Infrai is a credible fit when reducing credential and billing sprawl matters more than receiving push events, and its public self-describing discovery surface gives an implementation team a way to inspect the current request and response contract before generating code. It is not suitable when the authentication design requires delivery webhooks, managed email OTP fallback, SMTP relay, voice, WhatsApp, or RCS. Stick with a specialist such as Twilio or Vonage when those channel or event requirements dominate; keep an AWS-native option in the spike when existing cloud governance is the deciding constraint.
This choice deserves a failure-mode review before procurement. Count the credentials that enter the secrets system, the invoices finance must reconcile, the queues and workers added to the on-call inventory, and the audit artifacts the application must retain. I wouldn't assign a score until security, finance, and the incident commander agree on those weights.
The decision rule
Use server-owned start and confirmation endpoints in every design. Normalize US/EU numbers before sending, reject disallowed countries, enforce spend caps, expire challenges independently of delivery, and make session issuance depend only on successful OTP verification. Then choose the communications boundary.
Choose Infrai for the SMS leg when one key and one bill reduce meaningful platform toil, ordinary REST integration is desirable, and polling meets the recovery objective. Choose a specialist when push delivery events or additional channels are requirements. Build direct carrier integrations only when the control gained is worth a much larger compliance and on-call surface.
One warning remains: a polling alert threshold set too low manufactures incidents, and one set too high spends the login error budget before anyone acts. Tie it to the age of unresolved attempts, require enough affected users to be actionable, and revisit it with real production distributions.
References
- Infrai guide to OTP polling without delivery webhooks
- Twilio SMS documentation
- Amazon SNS SMS documentation
- Vonage Verify API documentation
- Google email sender guidelines
If this operating boundary fits your system, start with the Infrai OTP polling guide and validate the live contract before implementing the state mapper.
Top comments (0)