One-Time Passwords (OTPs) sent via SMS or Email are crucial for securing critical workflows like registration, password resets, and high-value payments. However, OTP services are highly targeted by bad actors for SMS toll fraud and brute-force attempts. A secure OTP service requires strict rate limits, automated provider failovers, and low-latency storage.
Backing the service with Redis allows us to implement fast, atomic transaction checks while setting precise TTLs (Time-To-Live) on OTP codes.
1. Implementing Atomic Lockout in Redis
To prevent SMS spamming, we store two distinct keys in Redis for each mobile number: a cooling-down lock key (e.g., 60 seconds) and the actual OTP payload key (e.g., 5 minutes). We use Redis pipelines to check and set these values atomically:
func (s *OTPService) GenerateOTP(ctx context.Context, phone string) (string, error) {
lockKey := fmt.Sprintf("otp:lock:%s", phone)
otpKey := fmt.Sprintf("otp:code:%s", phone)
// Check if lockout is active
exists, _ := s.redis.Exists(ctx, lockKey).Result()
if exists > 0 {
return "", ErrLockoutActive
}
code := generateSixDigitCode()
pipe := s.redis.TxPipeline()
pipe.Set(ctx, lockKey, "1", 60*time.Second)
pipe.Set(ctx, otpKey, code, 5*time.Minute)
_, err := pipe.Exec(ctx)
return code, err
}
2. Multi-Provider Failover Strategy
If an SMS gateway (e.g., Twilio or Infobip) experiences an outage, the service should automatically route messages to a secondary provider. We implement a Circuit Breaker pattern that tracks the success rate of outbound SMS calls and redirects traffic when failures exceed a set threshold.
Using Go's channels and goroutines, we can dispatch SMS requests asynchronously without blocking the user registration API response, keeping user latency minimal.
Atomic Redis Lua Scripts for OTP Verification & Attempt Counting
Evaluate OTP attempt increments, TTL checks, and code comparisons inside a single atomic Redis EVAL Lua script to prevent timing and brute-force race conditions.