DEV Community

William QiBin
William QiBin

Posted on

Your OTP SMS Didn't Fail at Sending — It Failed at Delivery: 4 Checks Developers Miss

Hi, I'm William. I work in CPaaS / A2P messaging, mostly on international SMS delivery for verification, notifications, and alerts.

The most common bug report I hear from developers is:

"The API returned success, but the user never got the code."

That success usually only means the message was accepted. It says almost nothing about whether it was delivered — or delivered in time to matter. For OTP, a code that arrives after 60 seconds is effectively a failed code.

Here are the 4 checks I run first.

1. Route: Is there a direct route to that carrier?

A cheap route is not a cheap message if it never lands.

For international OTP, ask:

  • Is this a direct carrier connection, or is it hopping through aggregators?
  • Is the sender ID allowed in the destination country? Many markets reject or rewrite unregistered sender IDs.
  • Is this route built for OTP traffic, or for bulk marketing traffic? They fail in different ways.

Developer check: Don't test with one number in one country and call it done. Test your top 3 user countries separately. Delivery behaviour is per-country, per-carrier — not global.

2. Speed: Measure delivery time, not send time

Log two timestamps, not one:

t0 = when your system requested the send
t1 = when the delivery receipt (DLR) says "delivered"
delivery_time = t1 - t0
Enter fullscreen mode Exit fullscreen mode

If you only log your API call, you're measuring your own server — not the user's experience.

A practical alert I like:

# Pseudocode: flag slow OTP delivery for investigation
if delivery_time_seconds > 30:
    log_warning("slow_otp_delivery", country=country, carrier=carrier)
if delivery_status != "DELIVERED":
    trigger_fallback()  # voice OTP, another route, or a retry with a new code
Enter fullscreen mode Exit fullscreen mode

3. Receipts: Treat the delivery receipt as part of your product

No DLR handling means you're flying blind. At minimum, handle:

Receipt state What it usually means What to do
DELIVERED Handset received it Stop retrying, mark verification as sent
UNDELIVERED / FAILED Carrier or handset rejected it Check number format, sender ID, and route
EXPIRED Sat too long in transit Your route is too slow for OTP — change it
No receipt at all Provider gives you no visibility That's a provider problem, not a user problem

Wire DLR webhooks into the same dashboard where you watch signup conversion. A sudden drop in delivered rate in one country almost always shows up there before users start complaining.

4. Support: Can a human look at one message ID?

When a single important user can't log in, "check our status page" is not support.

Before you commit to a provider, test this in the trial — not after you sign:

  1. Send a real message.
  2. Give support that one message ID.
  3. See if they can tell you the carrier, the delivery state, and why it failed.

If they can't trace one message, they can't help you debug a country-wide drop either.

A quick pre-launch checklist

  • [ ] Phone numbers normalised to E.164 (+ + country code) before sending
  • [ ] Delivery time (not just send time) logged per country
  • [ ] DLR webhook handled, including EXPIRED and UNDELIVERED
  • [ ] Fallback ready: second route, voice OTP, or WhatsApp where it makes sense
  • [ ] One-message trace tested with support using a real message ID

Disclosure: I work at Chuanglan (a CPaaS provider). If you want a sandbox to run the checks above, new accounts currently get 100 free SMS messages + a small API credit to test with: https://www.chuanglan.com/control/regist?exclusive_code=LKFHZWBU — no card needed for the trial. Either way, run the 4 checks on whatever provider you use now. Most "mystery" OTP failures turn out to be check #1 or #3.

What does your OTP failure dashboard look like today — do you track delivery time per country, or just send success rate? I'd genuinely like to compare notes in the comments.

Top comments (0)