# Why your sign-up verification email arrives late: a checklist

> A verification email arrives late when the delay is in your queue, in the provider, or at the receiving server. Time each hop to find which, then fix that one.

- Published: 2026-11-07
- Author: Koltrix Team
- Category: Email API
- Source: https://koltrix.com/blog/why-your-sign-up-verification-email-arrives-late-a-delay-debugging-checklist

A verification email that arrives late has one of three causes: your app held it, your email provider held it, or the receiving mail server held it. You find which by writing down four timestamps for one test sign-up and seeing where the gap opens. This post gives you that method, then the usual fix for each hop.

Late is a different problem from missing. If the message never shows up, start with [why an email is not received](/blog/email-not-received-debugging). Here the message does arrive, but after the user has already clicked "resend" twice.

## How late is too late for a one-time code?

For a sign-up code, anything over about 30 seconds feels broken to the user, and a code that outlives its expiry is useless. There is no standard for the perceived limit, so treat it as a product decision and measure against it.

The expiry is the hard edge. NIST's [SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) recommends that an out-of-band secret sent by email or SMS stay valid for ten minutes or less. If your codes live five or ten minutes, a delay of a few minutes already eats most of the window. See [magic links versus email OTP](/blog/magic-links-vs-email-otp) for how that choice changes what a delay costs.

## Which four timestamps tell you where the delay is?

Record four moments for one test sign-up and compare the gaps.

| Timestamp | Where to read it | What a big gap after it means |
|---|---|---|
| T1: user submitted the form | Your app log | (start) |
| T2: your app handed the message to the provider | Your app log, or the API response time | Gap T1 to T2: your own queue or code is slow |
| T3: the provider accepted it and began delivery | Provider event log or the message `Date` header | Gap T2 to T3: provider-side queueing |
| T4: the message landed in the mailbox | The top `Received` header in the delivered message | Gap T3 to T4: the receiving server deferred it |

Read T4 from the delivered message itself. The [Received headers guide](/blog/tracing-delivery-received-headers) shows how to read the hops from the bottom up and spot the one with the long jump between two timestamps.

## Is the delay in your own app?

If T1 to T2 is more than a second or two, the delay is yours. The usual causes are a background queue that is behind, a worker that sends mail only on a timer, or a send call made after other slow work in the same request.

Check these first:

- Queue depth and age of the oldest job in the queue that sends email.
- Whether verification mail shares a queue with bulk jobs such as digests or imports. Give sign-up mail its own queue or a higher priority.
- Whether a retry on a transient error waits too long before the first retry. A first retry after a minute is a long time for a code.

The design side is covered in [async email queue architecture](/blog/async-email-queue-architecture) and the retry settings in [outbound retry and backoff policy](/blog/outbound-retry-backoff-policy).

## Is the delay at your email provider?

If T2 to T3 is large, the message sat in the provider's queue before delivery was attempted. A provider can hold mail for several reasons: a rate limit on your account, a new sending domain being throttled while it builds reputation, or a spike in its own load.

What to do:

1. Look for the message in the provider's activity log and read the status and timestamps, not only "delivered".
2. Send verification mail through a path separate from marketing or bulk mail. Mixing them means a large campaign can sit in front of a single code.
3. If you are on a new domain, expect slower acceptance at first and ramp volume gradually.

Whichever provider you use, ask for per-message timestamps. Without them you can only guess at this hop.

## Is the receiving server deferring the message?

If T3 to T4 is large, the receiving server answered with a temporary failure and your sender had to retry. A deferral is an SMTP reply in the 4xx range that tells the sender to try again later. The retry schedule then decides how late the message is.

A common cause is greylisting: the receiver temporarily refuses mail from a sender it has not seen before and accepts the retry. [RFC 6647](https://www.rfc-editor.org/rfc/rfc6647) describes the technique. Sender retry timing is governed by [RFC 5321 section 4.5.4](https://www.rfc-editor.org/rfc/rfc5321#section-4.5.4), which says retry intervals should be at least 30 minutes and mail should not be given up on for at least 4 to 5 days. In practice many senders retry much sooner, but it shows why a single greylisted attempt can cost minutes.

The signs in a delivery log are a `4xx` reply followed by a later success. [Greylisting and deferrals](/blog/greylisting-and-deferrals) shows what those log lines look like and what receivers expect.

| Receiver reply | Meaning | What helps |
|---|---|---|
| `421` or `450` with a "try again later" text | Temporary refusal, often greylisting or load | Wait for the retry; keep the sending IP consistent |
| `451` with a rate-limit text | The receiver is throttling you | Lower your rate to that domain |
| `452` mailbox full | The recipient's mailbox is full | Nothing on your side; show a clear message in the app |

## What if only one receiving domain is slow?

When delay appears for one provider and not others, the cause is nearly always at that receiver. Test with a few addresses on different providers. If only one is late, look at its deferral text in your delivery log rather than at your code.

Corporate addresses add their own layer. A company gateway may scan links and attachments before it hands the message to the mailbox, which adds delay your sender cannot see or change. If sign-ups from work addresses are late far more than from consumer addresses, that is a likely reason.

## How do you stop a late code from hurting sign-ups?

You cannot remove every delay, so make the flow tolerate one.

- **Do not shorten the expiry to tidy up.** A delayed code that has already expired is a worse outcome than a slow one.
- **Make resend safe.** Let a second request issue a new code and say plainly that the older one stops working, so a late first email does not confuse the user. [Idempotency keys for email sends](/blog/idempotency-keys-email-sends) help when your own retries could double-send.
- **Show a real status.** "We sent a code to a***@example.com" beats a spinner, and a resend link after about 30 seconds is enough.
- **Alert on the gap.** Measure T1 to T4 on a test account on a schedule, and alert when it passes your limit.

For that last point, a real mailbox makes the measurement simple. Koltrix's `wait_for_email` on an agent mailbox waits up to 60 seconds for the message and returns the code, so a scheduled test can record how long the whole path took. The steps are in [testing sign-up emails end to end](/blog/test-sign-up-and-verification-emails-end-to-end-with-wait-for-email).

## Bottom line

A late verification email is a timing problem you can locate. Record when the form was submitted, when your app handed the message over, when the provider accepted it, and when it landed. The biggest gap names the hop: your queue, your provider, or the receiving server. Fix that hop, keep sign-up mail separate from bulk mail, and design the resend flow so one slow message does not cost you the sign-up.
