Auth & Security

OTP login that doesn't fall over: rate limits, resend windows, and provider failover

One-time-password login looks simple until real users start mistyping codes, hammering resend, and hitting it from three cities at once. Here's how we kept OTP sign-up fast and abuse-resistant across a multi-city launch.

By Team WebSync · · 4 min read

Mobile phone displaying a six-digit one-time password with a countdown timer ring

OTP sign-up is one of those features that takes an afternoon to demo and weeks to actually harden. It works perfectly the first ten times you test it, then someone taps "resend" eleven times in a row, or a bot starts requesting codes for random numbers to run up your SMS bill, and the feature that felt done reveals it was only the happy path.

We built OTP-based sign-up for a rental marketplace onboarding renters and property owners across three cities simultaneously - a launch that needed authentication fast enough not to lose an impatient visitor, and resilient enough not to be the thing that goes down on day one.

1. Rate-limit the request, not just the verify

Most OTP bugs live on the request side. Every unrestricted "send code" endpoint is a free SMS-bombing tool against whatever number an attacker types in - and a real cost, since you pay your SMS provider per message whether the recipient wanted it or not.

We cap requests per phone number (a short cooldown between sends, a daily ceiling) and per IP address independently, so neither a single abused number nor a single abusive client can burn through your SMS budget. Verification attempts get their own, separate limit - a handful of tries per code before it's invalidated outright, forcing a fresh request rather than an endless guessing window.

An OTP endpoint with no rate limit isn't a login feature, it's a paid SMS relay for anyone who finds it.

2. Make the resend window honest

A visible countdown before "resend" becomes clickable does two jobs at once: it stops the double-tap that generates two valid codes for the same session (confusing when the first one still arrives), and it buys real time for the SMS to land before the user assumes it failed and starts mashing the button.

We set the resend cooldown to slightly longer than our provider's typical delivery time, not to an arbitrary round number - if messages usually land in 8 seconds, a 30-second cooldown feels safe rather than punitive.

3. Plan for the SMS provider having a bad day

Every SMS gateway has outages and carrier-specific delivery problems - a code that never arrives is indistinguishable, from the user's side, from your app being broken. Routing through a single provider with no fallback means their incident becomes your incident.

  1. Send through a primary provider first, and track delivery/failure webhooks rather than assuming success on send.
  2. On a failure signal (or a timeout with no delivery receipt) within a short window, automatically retry the same code through a secondary provider.
  3. Log provider-level success rates per carrier - a specific carrier prefix failing consistently is a signal to route around it, not a one-off.

4. Keep the code itself boring

Six digits, numeric only, expiring in a few minutes, single-use, invalidated the moment a new one is requested for the same number. None of that is exciting, and that's the point - OTP security comes from short exposure windows and low guess budgets, not from a clever code format. A code stored as a salted hash server-side (never compared as plaintext, never logged) means a database leak doesn't hand out valid, unexpired codes.

OTP feels like a UI problem until the first abuse spike. Budget the rate limits and provider fallback before launch, not after the SMS bill arrives.

How do you rate-limit OTP login without blocking real users?

Apply independent limits on two axes: a cooldown plus daily cap per phone number, and a separate cap per IP address, so one abused number or one abusive client can't exhaust your SMS budget alone. Give verification attempts their own tight limit per code, and invalidate a code the moment a new one is requested.

Share this guideLinkedInXWhatsAppFacebook
All guides

Want this built for you?

Book a free consult - we'll scope it and give you a fixed price.