emailverifier.dev
Posts

SMTP Greylisting: Why a 451 Is Not an Invalid Email

| 7 min read | Usama Ejaz
Four SMTP attempts meet a timed greylisting gate; two loop for retry, one passes, and a changed source starts over.

SMTP greylisting is a temporary rejection, not evidence that an email address is invalid. A receiving server deliberately answers an unfamiliar delivery attempt with a 4xx reply, often a 451, and expects a normal mail server to try again later. If the retry is acceptable, delivery continues.

That distinction matters anywhere SMTP evidence becomes a product decision. A signup form, a list-cleaning job, or an email verification service should not turn a temporary 451 into a permanent mailbox failure. The defensible result is inconclusive, usually unknown, until stronger evidence arrives.

There is one important caveat: a 451 alone does not prove greylisting. SMTP uses 4xx replies for many temporary conditions, including rate limits, resource pressure, and policy deferrals. Greylisting is one possible explanation.

What does a greylisting exchange look like?

I built a small local simulator to make the mechanism concrete. It is an illustrative model, not a test against a real mailbox provider. The simulated receiver remembers a tuple made from the client IP, envelope sender, and envelope recipient. It requires the same tuple to be at least 30 minutes old before accepting it.

const minimumAge = 30 * 60_000;
const firstSeen = new Map();

function key(a) {
  return `${a.clientIp}|${a.mailFrom}|${a.rcptTo}`;
}

function receive(a) {
  const seenAt = firstSeen.get(key(a));

  if (seenAt === undefined) {
    firstSeen.set(key(a), a.at);
    return [451, "4.7.1 Try again later", "new tuple"];
  }

  if (a.at - seenAt < minimumAge) {
    return [451, "4.7.1 Try again later", "retry too soon"];
  }

  return [250, "2.1.5 Recipient OK", "known tuple"];
}

const attempts = [
  { minute: 0, clientIp: "192.0.2.10" },
  { minute: 10, clientIp: "192.0.2.10" },
  { minute: 35, clientIp: "192.0.2.10" },
  { minute: 40, clientIp: "192.0.2.11" },
];

for (const attempt of attempts) {
  const input = {
    ...attempt,
    at: attempt.minute * 60_000,
    mailFrom: "sender@example.com",
    rcptTo: "user@example.net",
  };
  const [code, reply, reason] = receive(input);
  console.log(
    `${String(attempt.minute).padStart(2, "0")}m | ` +
    `${attempt.clientIp} | ${code} ${reply} | ${reason}`
  );
}

I ran four hypothetical attempts through it. The IP addresses are from the documentation-only 192.0.2.0/24 range, and the domains are reserved examples.

00m | 192.0.2.10 | 451 4.7.1 Try again later | new tuple
10m | 192.0.2.10 | 451 4.7.1 Try again later | retry too soon
35m | 192.0.2.10 | 250 2.1.5 Recipient OK   | known tuple
40m | 192.0.2.11 | 451 4.7.1 Try again later | new tuple

The first attempt receives a temporary failure. The second repeats too soon. The third preserves the tuple, arrives after the illustrative delay, and succeeds. The fourth changes only the sending IP, so the receiver sees a new tuple and starts again.

Real implementations vary. RFC 6647 describes greylisting at several SMTP stages and several possible tracking keys. A receiver might consider the source IP, envelope sender, envelope recipient, or some combination. The 30-minute threshold above belongs only to this simulator.

Why does a 451 not mean the mailbox is missing?

SMTP reply classes carry the meaning. RFC 5321 section 4.2.1 defines a 4yz reply as a transient negative completion. The requested action did not happen yet, but the condition may be temporary and the sender is encouraged to try again.

A 5yz reply is permanent for the attempted command. Even then, the enhanced status code and text matter. An explicit 550 5.1.1 can be strong evidence that a mailbox does not exist. A policy rejection such as 550 5.7.1 says the current transaction is not allowed, not necessarily that the mailbox is absent.

A 451 therefore tells you that this attempt was deferred. It does not establish mailbox existence, mailbox ownership, future delivery, or the exact reason for the deferral.

How should a sending mail server react?

A real sending MTA should queue the message and retry later. RFC 5321 section 4.5.4.1 says retries must be delayed and discusses a general retry interval of at least 30 minutes, while also allowing more sophisticated strategies. It also describes a configurable give-up period that is generally four to five days.

Those are protocol-level defaults, not a reason to hammer the receiver every 30 minutes. A production sender should use its MTA's queue, respect provider guidance, and back off. Repeated immediate attempts can look abusive and will not satisfy a time-based greylist.

It also helps to keep the delivery identity stable. If each retry leaves from a different outbound IP, a tuple-based receiver may treat every attempt as new. That does not mean every multi-IP sender will fail. Some receivers aggregate addresses or use different keys. It means IP rotation is a real diagnostic clue when a message keeps getting deferred.

Should an email verifier simply wait and retry?

The strongest objection is reasonable: if a normal sender retries, why should a verifier stop at unknown?

Because verification and delivery operate on different clocks. An MTA already owns a durable queue and can keep retrying for hours or days. An API request usually needs to finish in seconds. Holding a signup open for a protocol retry window would be a poor user experience, while aggressive retries can trigger rate limits or policy blocks.

A verifier could schedule a later recheck, but that still may not reproduce the same tuple. Its outbound IP may change, the receiver's state may expire, or the original 451 may have had nothing to do with greylisting. A retry can add evidence, but it cannot make the first response conclusive after the fact.

This is why our SMTP verification guide keeps temporary and policy responses separate from confirmed mailbox failures. EmailVerifier.dev exposes four top-level outcomes: deliverable, risky, undeliverable, and unknown. A temporary SMTP response belongs in the uncertainty path, not the invalid-address bucket. The current response schema is documented in the API reference.

What should a signup or list-cleaning workflow store?

I would preserve the raw SMTP evidence alongside the normalized result. At minimum, keep the reply code, enhanced status code when available, the stage that produced it, the attempt time, and whether a later retry occurred. That lets you change policy without rewriting history.

Then separate three decisions:

  1. Mailbox evidence: 451 remains unknown. Do not suppress the address as invalid.
  2. Product access: choose whether an unknown result can continue, needs review, or requires a confirmation email. That depends on your abuse risk, not SMTP alone.
  3. Future delivery: let the sending system own its retry queue. Verification is not a replacement for actual delivery feedback.

Email confirmation is useful here, but it answers a different question. Clicking a single-use link can demonstrate control of an inbox at that moment. It does not retroactively prove why an earlier SMTP session returned 451. Our guide to secure email verification links covers that ownership layer.

How can you tell whether it really was greylisting?

Start with the sequence, not one line of text. A temporary rejection followed by acceptance of a materially unchanged retry after a delay is consistent with greylisting. Logs showing a reset when the client IP or envelope tuple changes make that explanation stronger.

But keep the wording careful. RFC 6647 explicitly warns that not every temporary failure is greylisting. A server can return the same 451 text for congestion, throttling, or a local policy. Even later acceptance only tells you the temporary condition cleared.

My practical rule is simple: treat 4xx as retryable transport evidence, keep it out of permanent mailbox-failure logic, and promote a result only when a later observation supports it. That preserves the uncertainty the protocol actually gave you.

Continue reading