Security8 min read

2FA Emails Not Arriving: Users Locked Out

A two-factor code that lands in spam is not a minor annoyance — it is a total lockout. No code, no access, no way to authenticate. And the time-sensitive nature of 2FA makes the filter problem worse.

Email 2FA codes exist somewhere on the deliverability difficulty spectrum between password resets and phishing. They are short, urgent, numeric, and triggered repeatedly for the same recipient. Every one of those traits hits a spam heuristic.

When a 2FA email lands in spam, your user cannot log in. Full stop. There is no graceful degradation. No retry that magically works. And because the code expires in 5-10 minutes, even a minor delivery delay is effectively a failure.

The heuristics working against you

  • Short body. "Your verification code is 382910. It expires in 10 minutes." Two sentences, one number, urgency language. Reads exactly like the phishing templates Bayesian filters were trained on.
  • Numeric code in subject line. "Your code: 382910" triggers an anti-phishing feature in Gmail that specifically looks for credential-like strings in subjects.
  • Rapid repeat sends. If a user clicks "resend code" three times, you send three nearly-identical short emails from the same sender to the same recipient in 60 seconds. Frequency heuristics flag this.
  • Urgency vocabulary. "verify", "secure", "expires", "immediately" — all phishing fingerprint words.
Users blame your app, not their spam filter

When 2FA fails, the user does not think "my spam filter ate the email". They think "this app is broken". They uninstall, chargeback, or leave a review. The cost of a missed 2FA email is measured in lost users and security complaints, not support tickets.

Measuring 2FA placement in practice

Fire the production 2FA flow with seed mailboxes, capture placement per provider, record time-to-inbox. A code that arrives 3 minutes after the 5-minute expiry is useless even if it is technically in the inbox.

# Trigger real 2FA flow to seed:
for seed in seeds.list(group='2fa-test'):
  start = time.now()
  trigger_2fa(seed.address)
  delivery = poll_placement(seed.address, timeout=120)
  print({
    "provider": seed.provider,
    "folder": delivery.folder,
    "seconds": (delivery.arrived_at - start).total_seconds()
  })

# Fail the build if:
# - any seed shows "spam"
# - any delivery > 30 seconds

Acceptable metrics

  • Placement. 99%+ inbox at Gmail, Outlook, Yahoo, iCloud.
  • Latency. Under 15 seconds p95, under 30 seconds p99.
  • Retry delivery. Same placement and latency on the second and third send to the same recipient.

Fixes ordered by impact

  1. Dedicated subdomain and sender. auth.yourapp.com with its own DKIM selector. Never share with marketing or notifications.
  2. Rewrite the template. Include context that phishing mail does not have: the user's name, the name of the device or browser triggering the login, the IP city. This differentiates legitimate 2FA from phishing in the body.
  3. Do not put the code in the subject line. Subject: "Sign-in request for your Acme account". Body contains the code. Gmail's anti-phishing heuristic that looks for codes in subjects will flag if you put it there.
  4. Add a plain-text alternative. Short HTML-only emails look thin. Include a text/plain part with the code, a one-line explanation, and a support contact.
  5. Use a dedicated IP if volume allows. Shared IPs at transactional ESPs bounce reputation around — one misconfigured customer on the same IP can tank your 2FA placement for an hour.
Ship TOTP alongside email

Email 2FA should not be your only second factor. Offer TOTP (Google Authenticator, Authy) as a primary option, with email as fallback. TOTP has zero delivery risk. For high-value accounts, offer WebAuthn too.

Template that tends to inbox

Subject: Sign-in request from Chrome on macOS (London, UK)

Hi {{first_name}},

We received a sign-in request for your Acme account from:
  Device: Chrome 121 on macOS
  Location: London, UK (approximate, from IP)
  Time: {{timestamp_iso}}

If this was you, enter this code on the Acme sign-in page:

    {{code}}

The code expires at {{expiry_iso}} ({{expiry_human}}).

If you did NOT try to sign in, change your password now:
https://yourapp.com/account/security

--
Acme Security Team
123 Example Street, City, Country
support@yourapp.com

Notice: full text-heavy body, context about the login request, named support team, physical address, plain-text URL. Structurally the opposite of phishing despite containing a one-time code.

Infrastructure checks

  • SPF ~all, not -all initially. If you miss an include, hard fail (-all) will reject legitimate 2FA. Start with soft fail, monitor DMARC reports, then tighten to -all once clean.
  • DKIM key length 2048 bits. 1024-bit keys still work but are increasingly penalised by Gmail and Outlook.
  • DMARC p=quarantine or p=reject. A missing DMARC policy is a deliverability handicap for critical auth mail.
  • MTA-STS and TLS reporting. Publish _mta-sts.yourapp.com. Providers are increasingly requiring TLS-enforced delivery for transactional mail.
Test after every template change

Your 2FA template gets touched more than you think — i18n updates, branding tweaks, legal footer changes. Every one is a potential placement regression. Wire an automated test into the auth service CI.

→ Run the free Inbox Placement Test

FAQ

Why does the first code arrive but retries don't?

Rate-based filtering. A burst of identical short emails from the same sender to the same recipient within 60 seconds triggers frequency heuristics. Solution: back-off on the resend interval or include a token in each email body that changes the content hash.

Are SMS 2FA codes better?

For deliverability, yes, but SMS has its own issues: SIM swap attacks, international delivery costs, and user friction. The defensible design is TOTP primary, email or SMS fallback.

Can I just use longer codes?

Longer codes are harder to phish but have identical email deliverability. The placement issue is the template and sender, not the length of the code.

Should I send 2FA emails from a different ESP than marketing?

Yes, if volume justifies the operational cost. Separate ESP, separate subdomain, separate IP. Isolation protects critical auth mail from marketing missteps.
Related reading

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required