March 28, 2026 MOBITELSMS Engineering 13 min read

SMS-based one-time passwords remain the most widely deployed second factor for user authentication. Despite ongoing debate about their security relative to authenticator apps or hardware keys, SMS OTP is used by billions of services because it works on every phone, requires no app installation, and has near-universal user familiarity. This guide covers how to implement SMS OTP correctly -- the generation algorithm, the delivery pipeline, the security considerations, and the cost realities.

What Is SMS OTP Authentication?

SMS OTP authentication sends a short numeric or alphanumeric code to a user's phone number via text message. The user enters that code into your application to prove they control the phone number. This serves two purposes:

The basic flow is straightforward: your application generates a code, sends it via SMS, and then verifies the code when the user submits it. The complexity is in doing each of these steps correctly and at scale.

OTP Generation Methods

There are three common approaches to generating OTP codes. Each has different characteristics for security, statelessness, and implementation complexity.

Random OTP (Most Common for SMS)

The simplest approach: generate a random number, store it server-side with an expiry timestamp, and send it to the user. When the user submits a code, compare it against the stored value.

# Python example -- random OTP generation
import secrets
import time

def generate_otp(length=6):
    """Generate a cryptographically random numeric OTP."""
    # Use secrets module, NOT random -- random is not cryptographically secure
    code = ''.join([str(secrets.randbelow(10)) for _ in range(length)])
    return code

def create_otp_record(phone_number, ttl_seconds=300):
    """Create and store an OTP with expiry."""
    code = generate_otp(6)
    record = {
        'phone': phone_number,
        'code': code,
        'expires_at': time.time() + ttl_seconds,
        'attempts': 0,
        'max_attempts': 3
    }
    # Store in Redis with TTL for automatic cleanup
    # redis.setex(f"otp:{phone_number}", ttl_seconds, json.dumps(record))
    return code

Critical detail: use secrets.randbelow() or random_int() (PHP) or crypto.randomInt() (Node.js) -- cryptographically secure random number generators. Never use Math.random(), rand(), or random.randint() for security-sensitive code generation. These functions use pseudo-random number generators that can be predicted if an attacker observes enough outputs.

TOTP (Time-Based One-Time Password)

TOTP, defined in RFC 6238, generates a code from a shared secret and the current time. This is what authenticator apps (Google Authenticator, Authy) use. The code changes every 30 seconds, and both sides can compute it independently without communication.

TOTP is rarely used for SMS OTP because it requires a pre-shared secret (which the user would need to set up initially), and the 30-second window does not align well with SMS delivery latency, which can range from 2 to 60 seconds depending on the route. If the SMS takes 35 seconds to arrive, the code may have already expired. You can extend the window, but that weakens the security guarantee.

HOTP (HMAC-Based One-Time Password)

HOTP, defined in RFC 4226, generates a code from a shared secret and a counter that increments with each use. Like TOTP, it requires a pre-shared secret. HOTP is used in some hardware token implementations but is uncommon for SMS OTP for the same reason as TOTP -- the pre-shared secret requirement adds setup complexity.

For SMS OTP, the random approach is almost always the right choice. It requires no pre-shared state, works immediately, and the server-side storage is trivial (a Redis key with a TTL).

OTP Message Format Best Practices

The message content matters more than you might think. Poorly formatted OTP messages reduce verification rates, confuse users, and can trigger carrier spam filters.

Recommended Format

Your ACME verification code is: 847291. It expires in 5 minutes. Do not share this code.

Key elements of a good OTP message:

Support for Autofill

Modern mobile operating systems can automatically detect OTP codes in SMS messages and offer to autofill them into the requesting app. To support this:

Delivery Infrastructure

OTP messages have different requirements than bulk marketing SMS. They must arrive quickly (under 10 seconds ideally), reliably (failure means the user cannot log in), and they tend to be sent one at a time rather than in batches.

SMPP vs HTTP API

Two primary protocols are used for sending SMS programmatically:

Delivery Priorities

OTP messages should be sent on high-priority routes with direct carrier connections. Key considerations:

Expiry Times and Retry Logic

OTP codes should have a limited lifetime. The expiry window balances security (shorter is better) against usability (the user needs time to receive and enter the code).

Retry Logic

Users will request new codes if the first one does not arrive quickly. Your retry logic should:

  1. Invalidate the previous code -- When a user requests a new OTP, the old one should be immediately invalidated. Do not allow both to be valid simultaneously.
  2. Rate limit resend requests -- Allow a maximum of 3-5 resends per phone number per hour. After that, lock the number out temporarily and suggest an alternative verification method.
  3. Introduce increasing delays -- First resend available after 30 seconds, second after 60 seconds, third after 120 seconds. This discourages rapid-fire requests and gives the SMS system time to deliver.
  4. Try a different route on retry -- If the first delivery attempt appears to have failed (no DLR within 15 seconds), route the retry through a different carrier path.

Rate Limiting and Abuse Prevention

OTP endpoints are prime targets for abuse. Without rate limiting, an attacker can use your OTP system to harass phone numbers with unwanted messages (SMS bombing), drain your SMS budget, or attempt to brute-force codes.

Rate Limits to Implement

Verification Attempt Limits

Limit the number of incorrect code submissions. A 6-digit numeric code has 1,000,000 possible values. With unlimited attempts, brute force is trivial. With 3 attempts, it is impossible.

# PHP example -- OTP verification with attempt limiting
function verify_otp(string $phone, string $submitted_code): bool {
    $key = "otp:" . $phone;
    $record = json_decode(redis()->get($key), true);

    if (!$record) return false;  // No OTP exists
    if (time() > $record['expires_at']) {
        redis()->del($key);
        return false;  // Expired
    }
    if ($record['attempts'] >= $record['max_attempts']) {
        redis()->del($key);
        return false;  // Too many attempts
    }

    $record['attempts']++;
    redis()->setex($key, $record['expires_at'] - time(), json_encode($record));

    if (hash_equals($record['code'], $submitted_code)) {
        redis()->del($key);  // One-time use: delete after success
        return true;
    }

    return false;
}

Note the use of hash_equals() for timing-safe comparison. A regular string comparison (== or ===) can leak information about how many characters matched through timing side channels. While this is harder to exploit for short numeric codes than for passwords, it is a good practice to follow.

SIM Swap Fraud Mitigation

SIM swap attacks are the most significant threat to SMS OTP security. In a SIM swap, an attacker convinces a mobile carrier to transfer the victim's phone number to a new SIM card controlled by the attacker. All subsequent SMS messages -- including OTP codes -- go to the attacker's device.

Detection Strategies

Fallback Channels

For high-security applications (banking, cryptocurrency exchanges), do not rely solely on SMS OTP. Offer and encourage:

Regulatory Requirements

SMS OTP implementations must comply with several regulatory frameworks depending on your market:

Cost Optimization

OTP messages are typically small (under 160 characters, single segment) and transactional, but at scale they represent a significant messaging cost. Strategies for managing OTP costs:

SMS OTP vs. Authenticator Apps

The security community has largely moved toward recommending authenticator apps over SMS OTP. The comparison:

The practical conclusion: offer authenticator apps as the recommended option, but always provide SMS OTP as a fallback. For phone number verification (as opposed to login 2FA), SMS remains the standard because the purpose is to verify the number itself.

Implementation Checklist

  1. Use cryptographically secure random number generation for codes
  2. Generate 6-digit codes (minimum) -- 4-digit codes are too easy to brute-force even with attempt limits
  3. Set expiry to 5 minutes
  4. Limit verification attempts to 3 per code
  5. Invalidate the code immediately after successful verification
  6. Rate limit OTP requests per phone number, per IP, and globally
  7. Use timing-safe comparison for code verification
  8. Monitor for SIM swap indicators on high-value accounts
  9. Include sender identification, expiry notice, and anti-sharing warning in the message
  10. Support autofill formats for Android, iOS, and web
  11. Implement delivery receipt monitoring with automatic route failover
  12. Log OTP events for fraud analysis (but never log the code itself)

SMS OTP is not going away anytime soon. Implemented correctly -- with proper code generation, rate limiting, fraud detection, and fallback channels -- it provides a practical and effective layer of account security that works for virtually every user on every device.