Short Answer
OTP expiration is not only a security setting. It is also a product setting. Too short and real users fail because their inbox is slow. Too long and old messages remain useful long after the user has moved on.
The TTL Tradeoff
Too short
Users wait for email, switch apps, find the code, and discover it already expired.
Too long
Old inbox messages remain valid longer than the product action really needs.
Rolling TTL
Every resend changes the time window and can accidentally keep a challenge alive.
Stable TTL
The active challenge has one clear expiration, and every resend points to it.
Resend Should Not Surprise Expiration
Resend should reuse the active challenge and tell the user how much time remains. That keeps the state machine clear: the user has one active code, one expiration, and one attempt budget.
if activeChallenge.expiresAt > now:
sendEmail(activeChallenge.code)
return {
reused: true,
expires_at: activeChallenge.expiresAt
}If your product extends expiration on resend, set a hard maximum lifetime. Repeated clicks should not keep the same challenge open indefinitely.
Adjust By Purpose Only When Needed
Different purposes can have different risk levels. A newsletter confirmation and an account deletion prompt do not need the exact same policy, but every extra policy is another branch to explain, test, and monitor.
- Start with a single 10-minute default for email OTPs.
- Shorten high-risk actions only when the UX still works.
- Keep generated report and private content flows long enough for inbox latency.
- Show expired states clearly so users know to request a new code.
- Do not accept expired codes even if the user still has attempts left.
Store Expiration On The Challenge
Expiration should be part of the challenge record, not inferred from the latest email send. Store the code, purpose, created time, expiration time, status, and failed attempt count together.
challenge = {
key,
code_hash,
purpose,
status: "active",
attempts_failed: 0,
created_at,
expires_at: created_at + 10 minutes
}Default policy
One active code. One expiration. Ten minutes.
sendotp.email is built around stable 10-minute email OTPs so resend behavior stays simple for users and predictable for developers.
FAQ
How long should an email OTP be valid?
A 10-minute TTL is a practical default for email OTPs because it gives real inboxes time to deliver while keeping the challenge short-lived.
Should resend reset the OTP expiration?
Usually no. Resend should reuse the active code and expiration. If you extend expiration, cap the total lifetime so repeated resends cannot keep a challenge alive forever.
Should login OTPs expire faster than email verification OTPs?
They can, but complexity has a cost. Start with one clear default, then shorten or lengthen specific purposes only when the product risk justifies it.
What happens after an OTP expires?
Verification should fail with an expired state, and a new send request can create a fresh challenge with a new code.