Delivery Speed Requirements
2FA emails are the most time-sensitive email type. Users are waiting on a login screen and will abandon the flow if the code doesn’t arrive quickly.| Delivery Target | User Experience |
|---|---|
| Under 5 seconds | Seamless — user barely notices the wait |
| 5–15 seconds | Acceptable — user waits but stays on the page |
| 15–30 seconds | Frustrating — user considers requesting a new code |
| Over 30 seconds | Failure — user abandons the login attempt or contacts support |
Optimizing for Speed
- Send immediately — never queue 2FA emails behind other sends. Use the highest priority in your sending pipeline.
- Keep the email tiny — minimal HTML, no images, no tracking pixels. Every byte adds processing time at each hop in the delivery pipeline.
- Use a warmed, reputable domain — established sending domains are processed faster by receiving servers. See IP and Domain Warm-Up.
- Monitor delivery latency — alert if the time between your API call and the
email.deliveredwebhook event exceeds 10 seconds.
OTP Generation and Security
Generate Secure Codes
Use a cryptographically secure random number generator. The code should be numeric (for easy entry on mobile) and 6–8 digits:Don’t use
Math.random() for OTP generation. It’s not cryptographically secure and produces predictable sequences that an attacker could guess.Set Short Expiry Times
OTP codes should expire quickly. The user is actively waiting to enter the code:| Expiry | Recommendation |
|---|---|
| 5 minutes | Recommended default |
| 10 minutes | Maximum — only if your users have slow email delivery |
| 30+ minutes | Too long — significantly increases the attack window |
Invalidation Rules
- Single use — invalidate the code immediately after successful verification
- New code invalidates old — when a user requests a new code, invalidate all previous codes for that user
- Maximum attempts — lock the account temporarily after 5 failed verification attempts
Email Content
2FA emails should be the simplest emails you send. The user needs the code and nothing else.Template Structure
Design Principles
- Large, monospaced code — make the code easy to read and distinguish similar characters (0 vs O, 1 vs l)
- Letter spacing — add space between digits so users can read them in groups
- No clickable links for the code — the user should type the code manually, not click a link (clicking a magic link is a different authentication pattern)
- Minimal content — no marketing, no navigation, no images beyond a simple logo
- Clear expiry notice — tell the user how long the code is valid
- Security warning — if they didn’t request the code, tell them what to do
Plain-Text Version
Sending with Lettr
Rate Limiting
2FA emails are a common target for abuse — attackers may trigger OTP sends to flood a user’s inbox or exhaust your sending quota.Implement Rate Limits
| Limit | Recommended Value |
|---|---|
| Per email address | 5 codes per 15 minutes |
| Per IP address | 10 codes per 15 minutes |
| Global per account | 20 codes per hour |
Fallback Strategies
Email delivery is not guaranteed to be fast enough for 2FA. Plan for failures.Resend Option
Provide a “Resend code” button on the verification screen, but enforce a cooldown:- 30-second cooldown between resend requests
- Maximum 3 resends per verification session
- Generate a new code on each resend (invalidating the previous one)
Alternative Channels
Consider offering fallback options when email delivery is slow:- Authenticator app (TOTP) — no delivery delay, works offline
- SMS — faster delivery than email in most cases, but has its own reliability issues
- Backup codes — pre-generated codes the user stored during setup
Email-based 2FA is less secure than authenticator apps (TOTP) because email can be intercepted or the inbox compromised. If your application handles sensitive data, offer TOTP as the primary option and email as a fallback.
Common Mistakes
Code is too short
Code is too short
A 4-digit code has only 10,000 possible values — an attacker with no rate limiting can brute-force it quickly. Use 6 digits minimum, and enforce strict attempt limits.
Code doesn't expire
Code doesn't expire
A non-expiring OTP gives attackers unlimited time to intercept and use it. Set a 5-minute expiry as the default.
Including tracking pixels
Including tracking pixels
Tracking pixels add latency and size to the email. For 2FA, delivery speed matters more than open tracking. Disable tracking for OTP emails.
Sending from a cold or shared domain
Sending from a cold or shared domain
Not rate limiting OTP requests
Not rate limiting OTP requests
Without rate limits, an attacker can trigger thousands of OTP emails, flooding the user’s inbox and burning through your sending quota.
Storing the raw OTP in the database
Storing the raw OTP in the database
If your database is compromised, raw OTP codes give attackers immediate access. Hash the code before storing it, just like passwords.
Using the same code for multiple actions
Using the same code for multiple actions
An OTP generated for login should not be valid for a password change. Bind each code to a specific action and validate the action context during verification.