Skip to content

Email one-time codes: expiry, length and what to tell users

A good email one-time code is six digits, expires in about 10 minutes, works once, and is rate limited. Here is how to choose each setting and word the email.

Koltrix Team5 min read
Macro shot of a stainless steel padlock
Photo by Jose Fontano on Unsplash
On this page(7 sections)
  1. How long should an email OTP be?
  2. How long should the code stay valid?
  3. What rules should the server enforce?
  4. What should the email say?
  5. What should happen when the code does not arrive?
  6. What are the common mistakes?
  7. Bottom line

An email one-time code is a short secret you send to an address so the person can prove they can read it, either to sign in or to verify the address. A solid default is six digits from a secure random generator, valid for about 10 minutes, usable once, with a hard cap on wrong guesses.

The delivery side matters as much as the numbers. A perfect 10-minute code is useless if the email takes four minutes to arrive, so this post covers both: the settings, then the message. If you are still deciding between codes and links, start with magic links vs email OTP codes.

How long should an email OTP be?

Six digits is the common choice, and it matches the floor in NIST SP 800-63B, which asks for out-of-band secrets of at least six decimal digits from an approved random generator. Six digits gives one million possible values. That is only safe because the code is short-lived and guesses are capped (see the attempt limit below).

Length Possible values Trade-off
4 digits 10,000 Too easy to brute force without very strict limits
6 digits 1,000,000 Easy to read and type; safe with expiry and attempt caps
8 digits 100,000,000 Safer against guessing, slower to type, more typos
Letters and digits Much larger Harder to read aloud and to autofill, easy to confuse 0 and O

If your product is low risk, six digits is right. If a code can authorize something sensitive, keep six digits and tighten the attempt limit rather than making users type eight.

How long should the code stay valid?

Ten minutes is a sensible ceiling. NIST SP 800-63B treats about 10 minutes as the maximum lifetime for an out-of-band code. For a sign-in code that the user is waiting for right now, five minutes is plenty, and a shorter window shrinks the time an attacker has to use a leaked code.

Pick the window from how fast your email actually arrives, not from a guess:

  1. Measure your 95th percentile time from "send requested" to "delivered" for these messages.
  2. Set expiry to at least that figure plus a minute or two for the person to switch apps.
  3. If you cannot keep delivery under a minute, fix delivery first. The delay debugging checklist for sign-up emails walks through the usual causes.

State the expiry in the email itself ("This code expires in 10 minutes") so nobody has to guess.

What rules should the server enforce?

Treat the code like a password reset token, because that is what it is. The OWASP Forgot Password Cheat Sheet covers the same ground for reset flows. The checklist:

  • Generate it with a cryptographically secure random generator, never a timestamp or a counter you can guess.
  • Store only a hash of the code, plus the address, creation time and an attempt counter.
  • Single use. Delete or mark the code used the moment it succeeds.
  • Cap wrong guesses. Five attempts per code is a common limit; after that, invalidate the code and require a new one.
  • Rate limit sending per address and per IP, so your form cannot be used to flood someone's inbox.
  • Only the newest code works. When the user asks for a resend, the old code stops working. Otherwise two valid codes exist at once.
  • Same response for known and unknown addresses on sign-in, so the form does not reveal who has an account.
  • Compare in constant time and bind the code to the address and the session that asked for it.

What should the email say?

Keep the message short enough to read in a notification preview. The code goes at the very top, large, and everything else is secondary.

Subject: 482913 is your Acme sign-in code

Your Acme sign-in code is:

482913

It expires in 10 minutes and works once.
If you did not ask for this, ignore this email. Your account is safe.

Why this shape works:

  • Code in the subject or first line. Many mail apps show the first line in the preview, so the user can read the code without opening the message.
  • Product name in the subject. A subject that is only a number looks like spam and is hard to find later.
  • No links to click. A code email with a link invites phishing imitation. If you must include one, make it a plain link to your site's sign-in page, not a tracked redirect.
  • Plain text part included. Send a text version alongside the HTML so the code survives any client. See plain-text versions of HTML email.
  • No open or click tracking on these messages. A rewritten link or tracking pixel adds a failure point and privacy cost for no benefit.

On the web form, make typing easy. Use autocomplete="one-time-code" and inputmode="numeric" on the input; MDN documents the one-time-code value, which lets supporting browsers and phones offer the code for autofill. Accept codes pasted with spaces, and show a clear error that says whether the code is wrong or expired.

What should happen when the code does not arrive?

Give the person a way forward without opening a hole:

  1. Show "Send a new code" after about 30 seconds, with a visible cooldown.
  2. Tell them to check spam and say which address you used, with a way to correct a typo.
  3. Log the send and the delivery result so support can answer "did you send it?" in one lookup.
  4. Offer a second route, such as a different address or a magic link, after a few failed tries.

For the logging, use delivery events from your sending provider. With Koltrix you send through POST https://api.koltrix.com/api/v2/emails with an API key, and the email logs and webhooks show whether the message was delivered, deferred or bounced; see the Koltrix docs. If you want to confirm the whole flow works before shipping, test sign-up and verification emails end to end with a real inbox.

What are the common mistakes?

Mistake Why it hurts Fix
Code valid for hours Leaked codes stay useful 5 to 10 minutes
Multiple codes valid at once Doubles the guessing surface Newest code only
No attempt cap One million values can be brute forced Invalidate after about five wrong tries
Code only in an image Not accessible, not autofillable Real text
Sending from a marketing stream Late or filtered delivery Use a transactional stream with authenticated sender
Vague "something went wrong" Users request code after code Separate "wrong" from "expired"

Bottom line

Use six random digits, expire them in 5 to 10 minutes, allow one use and a handful of guesses, and send only the newest code. Put the code at the top of a short plain message, say how long it lasts, and mark the input for autofill. Then watch delivery time, because the best settings fail if the email shows up late.

Start with Koltrix

Your domain, one inbox, and an API that sends.

A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

SharePost on XLinkedIn