← All posts

Reading an email header to find where the delay happened

6 September 2026 · 6 min read · Email

"The email took four hours to arrive." The header block holds the answer, and it is usually one hop that accounts for nearly all of it. The difficulty is that the block is long, repetitive and printed in an order that feels backwards.

Read it from the bottom up

Every server that handles a message prepends its own Received: line to the top of the header block. Nothing rewrites what is already there.

So the bottom Received: line is the first hop, written by the sender's own server, and the top one is the last hop, written by the recipient's. To follow the message in the order it actually travelled, read from the bottom upwards.

This also tells you something about trust. A server can only vouch for what it saw. Lines near the bottom were written by machines further from you and closer to a potential forger — and a spammer can invent as many Received: lines as they like before handing the message over. Only the lines added by servers you control, at the top, are reliable.

What one line contains

Received: from mail.sender.example (mail.sender.example [198.51.100.25])
        by mx.recipient.example (Postfix) with ESMTPS id 4Wq8Kt2Yz1z
        for <[email protected]>; Sat, 6 Sep 2026 09:14:02 +0000 (UTC)

Finding the delay

Take the timestamp from each Received: line and compare each with the one below it. The difference is how long the message sat at that hop.

Two traps make this harder than subtraction:

In practice one hop usually holds nearly all of the elapsed time. A multi-hour gap is almost always a receiving server that deferred the message — greylisting, a full queue, rate limiting, or a spam filter holding it for analysis. A gap of a few seconds spread evenly across hops is normal.

The authentication line

The receiving server records its verdict in one header:

Authentication-Results: mx.recipient.example;
  spf=pass smtp.mailfrom=sender.example;
  dkim=pass header.d=sender.example;
  dmarc=pass (p=reject dis=none) header.from=sender.example

Read the identifier attached to each result, not just the pass or fail. spf=pass tells you the envelope domain was authorised; smtp.mailfrom names which domain that was. dkim=pass with header.d names the signing domain. DMARC then checks those against header.from.

When SPF and DKIM both pass but DMARC fails, the domains did not align — a different problem with a different fix, covered in why SPF passes but DMARC still fails.

This header is written by the receiving server. If a message shows an Authentication-Results line from a server that is not yours, it proves nothing: anyone can add one.

Other headers worth a look

Where to get the raw headers

Forwarding an email does not carry its headers — you get the headers of the forward. You need the original source:

Headers contain recipient addresses, internal hostnames and internal IP addresses. Treat a header block as internal information before pasting it anywhere.

The Email Header Analyzer takes a raw header block, orders the hops chronologically, converts the timestamps and shows the delay at each one, alongside the SPF, DKIM and DMARC results.