What Phone Verification Accomplishes
Phone verification confirms that a user possesses a specific phone number at registration or for two-factor authentication. Unlike email, phone delivery is synchronous and near-instant, making it suitable for time-sensitive OTPs. The design must handle SMS delivery reliability, abuse prevention, international numbers, and the cost implications of SMS at scale.
OTP Generation: HOTP and TOTP
One-time passwords can be generated two ways. HOTP (HMAC-based OTP, RFC 4226) uses a counter: OTP = HOTP(secret, counter) where HOTP = truncate(HMAC-SHA1(secret, counter)), producing a 6-digit code. The counter increments on each new OTP request. TOTP (Time-based OTP, RFC 6238) replaces the counter with a time step: counter = floor(current_time / 30), producing a code that changes every 30 seconds. For SMS verification, HOTP with a per-request counter stored in Redis is simpler: you generate one code per verification attempt, store it, and verify against it. TOTP is more common for authenticator app scenarios.
For a 6-digit OTP, the search space is 1,000,000. Combined with a short TTL (10 minutes) and attempt limiting (5 tries max), brute force is not feasible.
OTP Storage in Redis
Key: verify_otp:{e164_phone_number}
Value: {
"otp": "847291",
"attempts": 0,
"expires_at": 1730000000
}
TTL: 600 seconds (10 minutes)
Store the OTP, attempt counter, and expiry in a single Redis hash. The Redis TTL automatically cleans up expired OTPs. On each failed verification attempt, increment attempts. If attempts reaches 5, delete the key and return a lockout error — the user must request a new OTP. This prevents brute-force enumeration of the OTP space.
Rate Limiting OTP Requests
Limit OTP request rate by phone number to prevent SMS flooding. Use two Redis counters: a short window (3 requests per 10 minutes) to prevent rapid resend abuse, and a longer window (10 requests per 24 hours) to limit daily cost exposure per number. Return HTTP 429 with retry-after when either limit is hit. Track by E.164-normalized phone number to prevent bypass via number formatting variations (+14155552671 vs 14155552671).
SMS Delivery via Twilio / AWS SNS
Validate the phone number before attempting delivery. Use a number validation API (Twilio Lookup, Google libphonenumber) to confirm the number is valid E.164 format and is a mobile number (not a landline, which cannot receive SMS). Reject invalid numbers before calling the SMS provider — this saves cost and gives faster user feedback.
Send via Twilio or AWS SNS with a delivery status callback webhook. Log the provider's message SID and initial status (queued, sent, delivered, failed, undelivered). Delivery failures (carrier rejection, unreachable number) should surface to the user as a retryable error with a clear message, not a generic “something went wrong.”
Verification Flow and Timing-Safe Comparison
Full flow: (1) User requests OTP for their phone number. (2) Server validates number format, checks rate limits, generates OTP, stores in Redis, sends SMS. (3) User enters 6-digit code. (4) Server retrieves stored OTP from Redis, increments attempt counter, compares codes using a constant-time comparison. (5) On match: mark phone as verified, delete OTP from Redis. (6) On mismatch: return error with remaining attempts count.
Use constant-time string comparison (e.g., hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing attacks. A character-by-character comparison that short-circuits on the first mismatch leaks information about how many leading digits are correct.
Voice Call Fallback
Some users cannot receive SMS: feature phones with full inboxes, corporate devices with SMS blocked, VOIP numbers. Offer a voice call fallback that reads the OTP aloud using text-to-speech. Twilio and AWS Connect both support this. Present the fallback option after one failed SMS delivery or explicitly in the UI. Voice calls are more expensive than SMS — rate-limit voice call attempts more aggressively (1 per 10 minutes).
SIM Swap Fraud Detection
SIM swap attacks occur when a fraudster ports a victim's number to a new SIM, then completes SMS-based authentication. Mitigations: (1) Check if the phone number was ported recently using carrier lookup APIs (Twilio offers this via the Lookup API with PortedDate). A number ported within the last 24–48 hours is high risk. (2) Check account age at the carrier — very new SIMs are suspicious. (3) For high-value actions (large withdrawals, password resets), consider requiring additional verification when SIM swap indicators are present.
International Handling and Cost Controls
SMS costs vary dramatically by country — a message to the US costs $0.0079 via Twilio, but messages to some countries cost $0.20+. International numbers can be used for SMS pumping fraud: bots register with premium-rate numbers and collect revenue from the SMS termination fees. Controls: geo-restrict SMS to countries where your service operates, maintain a blocklist of known premium-rate prefixes, set a daily cost budget per account, and flag accounts that verify multiple numbers in short succession.
Within the OTP validity window, if a user requests a resend, return the same OTP rather than generating a new one. This avoids sending two valid OTPs simultaneously and reduces delivery cost for impatient users who tap “resend” before the first SMS arrives.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How is a time-based OTP generated and validated?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “TOTP (RFC 6238) computes HMAC-SHA1(secret, floor(unixTime / stepSeconds)) and truncates the result to a 6-digit code, where the 30-second time step means both client and server independently derive the same value from a shared secret without any network exchange. Validation allows a ±1 step window to compensate for clock skew between the user's device and the server.”
}
},
{
“@type”: “Question”,
“name”: “How are rate limits enforced to prevent OTP abuse?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Rate limits are applied at multiple granularities: per destination phone number (e.g., max 5 SMS per hour) to prevent SMS pumping fraud, and per source IP (e.g., max 10 requests per minute) to prevent enumeration attacks, using a sliding window counter stored in Redis with an atomic INCR and TTL-based expiry. Breaching either limit should return a 429 with a Retry-After header rather than silently dropping the request.”
}
},
{
“@type”: “Question”,
“name”: “How does SIM swap fraud detection work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Carrier lookup APIs (such as Twilio's Lookup with SIM swap detection) return the date of the most recent SIM swap for a given MSISDN; if the swap occurred within a configurable recency window (e.g., 24 hours), the verification flow should step up to an additional identity challenge rather than trusting the SMS OTP. This is critical because a SIM-swapped number delivers OTPs to the attacker's device, rendering SMS-based 2FA ineffective without this check.”
}
},
{
“@type”: “Question”,
“name”: “Why must OTP comparison be timing-safe?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Naive string equality (==) short-circuits on the first mismatched character, leaking information about how many leading digits of the attacker's guess are correct via measurable response-time differences — a timing side-channel attack. A constant-time comparison (hmac.compare_digest in Python, MessageDigest.isEqual in Java) always iterates all bytes regardless of where the mismatch occurs, eliminating the timing signal.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems