"This email took an hour to arrive" is a complaint you can answer precisely, because every server that handled the message wrote a timestamped line into its header. Read those lines in the right order, convert the times, and the slow hop stands out. In this post I'll show how to get the header from the right copy, read the Received lines, use Microsoft's Message Header Analyzer, and confirm the finding with Exchange message tracking.
How it works#
- Each SMTP server that accepts a message adds a
Received:line at the top of the header. The first hop is therefore the bottom Received line, and the last hop is the top one. - Each line says who handed the message over (
from), who took it (by) and when, in that server's local time with its UTC offset, such as+0200. - The
Date:header is set by the sender's mail client, not by a server. A big gap betweenDate:and the first Received line means the message waited before it ever reached a mail server: an offline Outbox, deferred delivery or an application retrying. - Exchange also stamps organization headers that start with
X-MS-Exchange-Organization-, for exampleX-MS-Exchange-Organization-AuthAs(how the sender was authenticated:Anonymousfor internet mail,Internalfor your own servers and users),X-MS-Exchange-Organization-AuthSource(the server that accepted it) andX-MS-Exchange-Organization-SCL(spam confidence level). - The header firewall removes organization headers when mail crosses a connector that isn't trusted to keep them, so you won't see another organization's values. Some organizations also strip their internal Received lines from outbound internet mail, which hides those hops from external recipients.
Step-by-step#
1. Get the header from the recipient's copy#
The sender's Sent Items copy has never crossed the wire, so it tells you nothing about delivery. Use the recipient's copy:
- Classic Outlook: open the message in its own window, then File › Properties, and copy the Internet headers box.
- Outlook on the web: open the message's more actions menu (…) and view the message details.
2. List the hops bottom-up and convert to UTC#
Here is a simplified inbound header (protocol details shortened):
Received: from EX01.contoso.com (10.0.0.11) by EX02.contoso.com (10.0.0.12) with ...; <date> 11:12:24 +0200
Received: from gw01.contoso.com (10.0.1.5) by EX01.contoso.com (10.0.0.11) with ...; <date> 11:12:22 +0200
Received: from mail.fabrikam.com (203.0.113.25) by gw01.contoso.com (10.0.1.5) with ESMTPS; <date> 09:12:20 +0000
Received: from client.fabrikam.com (192.0.2.10) by mail.fabrikam.com with ESMTPSA; <date> 09:40:55 +0100| Hop (bottom-up) | Local time | UTC | Gap |
|---|---|---|---|
| Client → mail.fabrikam.com | 09:40:55 +0100 | 08:40:55 | start |
| mail.fabrikam.com → gw01 (your gateway) | 09:12:20 +0000 | 09:12:20 | 31 min 25 s |
| gw01 → EX01 | 11:12:22 +0200 | 09:12:22 | 2 s |
| EX01 → EX02 | 11:12:24 +0200 | 09:12:24 | 2 s |
Without converting, the hops look hours apart, or even out of order. In UTC the picture is clear: the half hour was lost before your gateway accepted the message. Either the sender's server held it, or your side deferred its attempts.
3. Let Message Header Analyzer do the arithmetic#
Microsoft's Message Header Analyzer parses the header, converts the times and flags suspected delays. Use the web version at mha.azurewebsites.net, or install the Outlook add-in from AppSource to analyze the selected message directly. Treat a slow hop as a lead, then confirm it in the logs.
4. Confirm the hop in message tracking#
Take the Message-ID: value from the header and search every transport server:
$id = "<CAF1x2y3z@mail.fabrikam.com>"
$t = Get-TransportService | ForEach-Object {
Get-MessageTrackingLog -Server $_.Name -MessageId $id -Start (Get-Date).AddDays(-2) -ResultSize Unlimited
} | Sort-Object Timestamp
for ($i = 1; $i -lt $t.Count; $i++) {
"{0} {1,-10} {2,-12} +{3:n0} s" -f $t[$i].Timestamp, $t[$i].EventId, $t[$i].Source,
($t[$i].Timestamp - $t[$i-1].Timestamp).TotalSeconds
}Good looks like a RECEIVE followed within seconds by SEND or DELIVER. Repeated DEFER events, or a long gap before DELIVER, place the delay inside Exchange. If the first RECEIVE is already late, the delay happened before Exchange accepted the message. My message tracking guide explains every event.
5. Match the gap to a cause#
| Where the time went | Usual causes | Check |
|---|---|---|
Between Date: and the first Received line | Offline Outbox, deferred delivery, application retries | The sending client or application |
| Inside the sender's own hops | Their queues or filtering | Send your hop table to their admin |
| Into your gateway or first Exchange server | Your side deferring the connection: back pressure, filtering or connection limits | SMTP receive protocol logs, gateway logs, back pressure |
Between your Exchange hops or before DELIVER | Transport queues, transport agents, database or delivery issues | Get-Queue, third-party transport agents |
| Outbound, after your last hop | DNS failures, timeouts to the next hop, recipient-side deferrals | 451 4.4.0 DNS, 441 4.4.1 timeouts |
Verify#
- After fixing the cause, send a test through the same path and read the new header: every hop should be seconds apart in UTC.
- Message tracking for the test shows
RECEIVEtoDELIVER(orSEND) withoutDEFERevents. Get-Queue | Where-Object { $_.MessageCount -gt 0 }shows no queue building up on any transport server.
Tips & gotchas#
- Trust only Received lines added by servers you control; anything below the first hop into your organization was written by others and could be forged.
- Clock drift makes hops look negative or out of order. Check time sync on your Exchange servers and gateways before blaming the network.
- Inside Exchange, an internal message still passes through the Transport service, so a message to a colleague on the same server has several hops too.
- Intentional waits look like delays: moderation, delayed delivery and a recipient's deferral of first attempts are all normal.
- Keep enough message tracking history: by default Exchange keeps up to 30 days.

