DEV Community

Cover image for Why OTP Verification Fails (and How to Fix It)
yobox
yobox

Posted on • Originally published at yobox.dev

Why OTP Verification Fails (and How to Fix It)

You typed the code. You're sure you typed it right. The site says "Invalid or expired code." You request a new one. Same thing.

OTP failures are one of the most frustrating UX patterns on the modern web, because they fail silently and the error messages are useless. Here are the seven real reasons OTP verification fails, in order of how often we actually see them in user reports and our own debugging.

1. The Code Already Expired

OTPs typically live 5–10 minutes. By the time you opened the email, read the code, switched tabs, and pasted it, you may have burned 9 minutes of the window — especially if the email was delayed in transit.

Fix: request a new code. Type it immediately. Don't go make coffee.

Developer note: if you're building an OTP flow, surface the exact expiry time in the email and the form. "This code expires at 14:32 UTC" beats "expires in 10 minutes" by a mile.

2. The Code Was Already Used

OTPs are typically single-use. If you mis-clicked Verify twice, or if a browser autofill submitted the form once and then again, the second attempt fails — even though the code is correct.

Fix: request a new code. Don't double-click. Watch for autofill.

3. The Email Was Silently Rejected

If you used a disposable email address, the site may have rejected your address at submission time without telling you. The form said "we sent a code"; nothing was actually sent.

Fix: if you suspect this, try with an email alias instead of a disposable address. Many sites accept aliases while blocking disposable domains.

4. The Email Went to a Spam Folder You Can't See

For temp mail users, this is silent death — temp inboxes don't have spam folders, so a filtered message is just gone. For real inbox users, it's a 30-second hunt.

Fix: check spam in your real inbox. For temp mail, regenerate the address and try again from a different provider. See "Receive OTP Codes with Temp Mail".

5. You're Verifying With the Wrong Address

Some flows let you request OTPs for any address you type, then verify by typing the address plus the code. If you typed the address slightly differently the second time (capitalization, trailing space, alias variant), the code won't match.

Fix: copy-paste both the address and the code. Never retype.

6. The Site's Clock Is Wrong

This is rare on modern infrastructure but still happens — especially on self-hosted apps and small SaaS. If the server's clock is skewed by more than the OTP TTL, valid codes appear expired.

Fix: nothing you can do from outside. Email the site's support.

7. Rate Limiting

If you've requested 5 codes in 10 minutes, most senders silently stop delivering. The code "didn't arrive" because the system refused to send it.

Fix: wait 10 minutes. Don't spam the "resend code" button.

How to Diagnose, in Order

If your OTP is failing, walk this list:

Did the email actually arrive? Check the timestamp.
Are you within the expiry window?
Have you tried this exact code before?
Is the email address you're verifying with exactly the one the code was sent to?
Did you request more than 3 codes in the last 10 minutes?
Are you using a disposable address that the site silently rejects?
Most failures resolve at step 1 or 2.

OTP Best Practices for Developers

If you're building an OTP flow, these are the patterns that hurt users least:

Use 6 digits, not 4. 4 digits is brute-forceable in seconds.
Set TTL to 10 minutes, not 60 seconds. A minute is hostile.
Allow at least 5 attempts per code. Typos happen.
Rate-limit at 3 codes per 10 minutes. Enough to handle real users, blocks abuse.
Don't invalidate the code on a typo. Invalidate after 5 failed attempts or on success.
Show the expiry time in the form. Not just "expires soon."
Send the code in the subject line. Mobile users see it without opening the email.
Echo the requesting IP / device in the email. Helps users spot account-takeover attempts.
For testing your own OTP flow, the YoBox Temp Mail tool gives you a disposable inbox in under a second; pair it with the Webhook Tester if your verify endpoint also fires backend webhooks (some auth providers send a "user.verified" webhook you'll want to assert on).

A Minimal Test Loop

If you're QA'ing OTPs in CI, this is the pattern:

test('signup with OTP', async ({ page }) => {
const { address, token } = await tempMail.createInbox();
await page.goto('/signup');
await page.fill('[name=email]', address);
await page.click('text=Send code');

const code = await tempMail.pollForCode(token, {
timeout: 30_000,
pattern: /\b(\d{6})\b/,
});

await page.fill('[name=otp]', code);
await page.click('text=Verify');
await expect(page).toHaveURL('/dashboard');
});
Enter fullscreen mode Exit fullscreen mode

Full walkthrough in "Email Testing Guide for Developers".

OTP vs Magic Links

A close cousin: magic links. Same idea (one-time token in email), different UX (you click instead of type). The failure modes are nearly identical, with one addition: magic links break when opened in a different browser than the one that requested them. If you request the link on desktop and open it on mobile, many implementations refuse.

For testing, YoBox Temp Mail handles both — you can extract a 6-digit code with a regex or fetch the link and follow it programmatically.

FAQ

Why does Discord say my OTP is invalid even when it's right?
Most often: you used a disposable address Discord silently rejected; the email never arrived. See "Temporary Email for Discord".

Why does the same code work in Chrome but not Safari?
Likely a cookie / session mismatch. The code is tied to a session ID stored in a cookie; switching browsers breaks the link.

Can I extend the OTP expiry?
Not as a user. As a developer, yes — but anything over 15 minutes hurts security.

Why does my OTP arrive twice?
Some senders retry on transient failures. Use the first code; the second is usually the same value resent.

Is SMS OTP more reliable than email OTP?
Generally yes for delivery speed (SMS is often instant), but SMS has its own failure modes (SIM swap, carrier filtering) and is more expensive for the sender.

Bottom Line

OTPs fail for boring reasons more often than exciting ones — expiry, double-clicks, blocklisted disposable domains, rate limits. Walk the diagnostic list before assuming the site is broken. If you're building OTP flows, optimize for human typos and slow inboxes, not the happy path.

YoBox Team

Builder behind YoBox — a privacy-first toolbox for developers and QA engineers covering disposable email, webhook capture, regex, secure passwords, Docker, and end-to-end testing.

Top comments (0)