Auth1BlogSMS OTP Rate Limiting
SecurityRate Limiting· 17 min read

Building Bulletproof SMS OTP Authentication

SMS OTP looks trivial to implement. Then your Twilio bill arrives at $47,000 because an attacker triggered 200,000 SMS messages to premium-rate numbers over a weekend. This guide covers the attack surface and the defense-in-depth architecture we use.

The Abuse Vectors

SMS Pumping (Toll Fraud): The attacker has a financial arrangement with a premium-rate number operator. They hit your "send OTP" endpoint in a loop. At 10 requests per second, that is $43,200 per day.

OTP Brute Force: A 6-digit OTP has 1,000,000 possible values. Without rate limiting, an attacker can try all codes in under 3 hours. With 10 parallel connections, 17 minutes.

Timing Attacks: Standard string comparison (===) short-circuits on the first mismatched character. An attacker can measure response times to determine correct digits, reducing search space from 10^6 to 10^3.

Replay Attacks: If an OTP is not invalidated immediately after successful verification, it can be used again within the expiry window.


Layer 1: Rust Token Bucket (In-Process, Zero Latency)

In-process token bucket implemented in Rust via auth-shield. No network call, no Redis lookup. Per-phone rate limiting: 3 OTP requests per phone number per 15 minutes. Caps SMS pumping cost at $0.90 per 15 minutes per number. Uses DashMap<String, TokenBucket> — lock-free concurrent hashmap with fine-grained sharding.


Layer 2: Redis Sliding Window (Distributed)

Shared across all instances behind a load balancer. Per-IP: 10 OTP requests per 15 minutes. Per-tenant: 100 OTP requests per hour. Global: 1,000 OTP requests per hour (circuit breaker). Catches distributed attacks that bypass per-instance limits.


Layer 3: Application-Level Escalating Lockout

Specifically for OTP verification (not sending). 1-4 failed attempts: no lockout. 5th failure: 1 minute lockout. 10th: 5 minutes. 15th: 15 minutes. 20+: 1 hour. Makes brute-force mathematically infeasible even for 4-digit OTPs.


Timing-Safe OTP Verification

auth-shield's verifyOtp() uses the subtle crate's ConstantTimeEq. When lengths differ, constant-time work is still performed to avoid leaking length info. Never use === for OTP comparison.


Production Checklist

  1. OTP length is 6+ digits. Never use 4-digit OTPs.
  2. OTPs expire in 5 minutes or less.
  3. OTPs are single-use. Delete from store immediately after verification.
  4. Verification is constant-time. Use verifyOtp(), not ===.
  5. Per-phone SMS rate limit exists. Maximum 3 per 15 minutes.
  6. Per-IP rate limit exists. Maximum 10 per 15 minutes.
  7. Per-tenant aggregate limit exists.
  8. Failed verification triggers lockout. Escalating after 5 failures.
  9. OTP codes are cryptographically random. Use crypto.randomInt(), not Math.random().
  10. Error messages do not leak information. "Invalid code" is fine. "Code expired 2 minutes ago" is not.

SMS OTP Done Right, Out of the Box

Auth1 handles rate limiting, constant-time verification, escalating lockout, and replay prevention. One API call.

Start Free →Read the Docs
Free tier · 1,000 verifications/month · No credit card required