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 Rehan Idrisi · · Updated · 5 min read
Part of: Custom Ecommerce
OTP sign-up is one of those features that takes an afternoon to demo and weeks to 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 finished turns out to have been only the happy path.
We built OTP-based sign-up for a rental marketplace onboarding renters and property owners across three cities simultaneously, and got a verified sign-up down to around 10 seconds end to end. The launch needed authentication fast enough not to lose an impatient visitor, and resilient enough not to be the thing that goes down on day one.
Most OTP bugs live on the request side. Any unrestricted "send code" endpoint doubles as a free SMS-bombing tool aimed at whatever number an attacker types in. It also costs you money, because you pay your SMS provider per message whether the recipient wanted it or not.
So we cap requests per phone number, with a short cooldown between sends and a daily ceiling, and cap per IP address independently. Neither a single abused number nor a single abusive client can burn through the SMS budget on its own. Verification attempts get their own separate limit: a handful of tries per code before it is invalidated outright, forcing a fresh request instead of leaving an endless guessing window open.
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.
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, which gets confusing when the first one still arrives. And it buys real time for the SMS to land before the user decides it failed and starts mashing the button.
Our resend cooldown is set slightly longer than the provider's typical delivery time, never to an arbitrary round number. If messages usually land in 8 seconds, a 30-second cooldown reads as safe instead of punitive.
Every SMS gateway has outages and carrier-specific delivery problems. From the user's side, a code that never arrives looks exactly like your app being broken. Route through a single provider with no fallback and their incident becomes your incident.
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 it shouldn't be. OTP security comes from short exposure windows and low guess budgets, never from a clever code format. Store the code as a salted hash server-side, never compared as plaintext and never logged, and a database leak stops handing out valid, unexpired codes.
Verification is the handoff into session issuance, and that step needs care of its own. On successful verification we issue a short-lived access token plus a longer-lived refresh token, and invalidate the OTP record at the same moment, not after the token is issued, so a valid code can never be replayed even inside a narrow race window.
New-device and new-city sign-ins get logged into the account's own activity history. A renter verifying from a phone that has never been seen against that account before shows up there, which gives a genuine owner a way to notice if their number was somehow verified from somewhere they weren't.
You can't load-test OTP delivery against a real SMS provider without either a large bill or a very understanding account manager. The rate-limiting and state-machine logic runs against a mocked provider instead, one that can be told to simulate delays, failures, or a specific carrier's known quirks on command. Real end-to-end sends are reserved for a small, controlled smoke-test batch before launch and after any change to the provider integration itself.
Not every phone number behaves like the common case a demo is built against. Landline-registered numbers, numbers on carriers with poor SMS-to-app delivery, and international numbers outside the primary launch markets each needed a fallback path of their own. Numbers with a history of failed SMS delivery get offered a voice-call OTP instead of a quiet retry down the same channel that already failed them.
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.
Decode, verify, parse, and edit JWT token components including headers, payloads, and signatures.
Generate SHA-1, SHA-256, SHA-512, and MD5 cryptographic hash values for strings in real time.
Generate cryptographically secure passwords and standard RFC 4122 UUID v4 strings instantly.