# 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.

- Published: 2026-11-07
- Author: Koltrix Team
- Category: Email API
- Source: https://koltrix.com/blog/email-one-time-codes-expiry-length-and-what-the-message-should-say

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](/blog/magic-links-vs-email-otp).

## How long should an email OTP be?

Six digits is the common choice, and it matches the floor in [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html), 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](/blog/why-your-sign-up-verification-email-arrives-late-a-delay-debugging-checklist) 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](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html) 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.

```text
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](/blog/plain-text-part-matters).
- **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](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete), 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](https://docs.koltrix.com). If you want to confirm the whole flow works before shipping, [test sign-up and verification emails end to end](/blog/test-sign-up-and-verification-emails-end-to-end-with-wait-for-email) 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.
