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.