Knowledge base

max-message-hops

<source>

How many Received headers a message may carry
in the configuration this is a linemax-message-hops 100inside the block <source>
Loop protection, counted by the number of hops

How many Received headers the node is willing to see in a message. Every mail system the message passed through adds such a header on top — forming a route trace. Too long a trace means the message is going in circles, and the node does not accept it.

This counts other systems’ traces, not its own. The node looks at what is already in the message at the moment of reception. The default of 100 is designed for normal mail, where hops are in single digits: a message with a hundred headers did not “travel long”, it looped.

Where loops come from. Usually from forwarding: mailbox A forwards to B, B is set to forward back to A — the message goes back and forth between them, and on each round the trace grows by two headers. The counter is the only thing that stops such a circle: the systems themselves do not see the loop.

The line lives in the submission block, which means it applies at reception. It has nothing to do with outgoing mail: a message the node sends is not checked by the counter.

The main trap. Only someone who sees the headers can count them, and the node is not obliged to add them. The neighboring line add-received-header set to false turns off adding its own Received — and our node stops lengthening the trace. A loop that spins through us then does not grow: the counter stays at the same number round after round and never reaches the threshold. The protection does not break, but it stops triggering on loops whose path goes only through this node.
What the Hub does. It does not write it — the vendor 100 stays.

🔴 But in the same submission block the Hub writes add-received-header false — deliberately, so as not to reveal internal addresses in the message. A side effect: our node does not add its own Received, and the loop counter does not grow on messages passing only through us. For our submission this is safe: the message arrives directly, from a submitting platform or from the Hub’s own warm-up, and is not forwarded anywhere. But it also means that loop protection here rests not on this line but on the fact that there is no forwarding in the setup.

Recommended values

There is no reason to touch the default. A hundred hops is certainly a loop, and normal mail never gets to that number. Lowering it makes sense as one-off diagnostics, not as an everyday setting.

It is lowered when hunting a loop. A value like 10 breaks the circle faster and shows it in the log sooner. After the investigation it is returned: a permanently low limit will start rejecting honest messages from long corporate chains.

Zero is not described here. The manual speaks only of a positive integer; what 0 does it does not explain, and that is worth checking on a test bench, not on a working node.

The advice about diagnostics is ours. From the manual come the default of 100, the value grammar and the fact that the count goes by Received headers.
Example configuration
# lowered limit for the duration of a loop investigation
<source 0/0>
    add-received-header true
    max-message-hops 10</source>
The top line is not here for decoration: without its own Received the node does not lengthen the trace, and a lowered limit will not trigger on a loop through this same node.
yellow — — directive names, blue — blocks. They are highlighted the same way in the text of the card.
▲

Common mistakes and problems

what people get wrong and how it ends3 mistakes▶

Lowering the limit with its own Received turned off

§ 3.3.7.6

What it costsThe counter does not grow from passing through our node, so a message looped through it will reach the threshold neither on the tenth round nor on the hundredth. The limit looks configured and does not work.
How to spot itThe same message appears in the queue again and again, and the number of Received headers in it does not change.
How to do it rightFor the duration of the investigation, set add-received-header back to true — then each round will be visible in the trace and the limit will trigger.

Keeping a low limit permanently

§ 3.3.7.38

What it costsLowering is useful for the duration of an investigation, but as a permanent setting it starts rejecting honest mail: messages from corporate chains with several gateways accumulate headers quickly and hit the limit without any loop.
How to spot itRejections at reception come from the same senders — usually organizations with their own mail gateways.
How to do it rightReturn the vendor 100 after the investigation. If the loop was not found, look for it in forwarding settings, not in the threshold.

Expecting the limit to stop an outgoing loop

§ 3.3.7.38

What it costsThe line lives in the submission block and checks messages at reception. A message the node sends that comes back to it from someone else’s system will be checked on the way in, not on the way out — if it comes back to us at all.
How to spot itA loop between two other systems continues even though the limit is set on our node.
How to do it rightInvestigate such a loop by the delivery log, not by a reception threshold: it has to be stopped by the system that forwards the message back.
Compatibility
PowerMTA 6.0
described in the manual of this version · platform: any
The vendor does not state the first version for every directive — there is no note here. The name also appears in the 5.0 edition of the manual.
Reference data
Syntax
max-message-hops <integer>
Values
a positive integer
Default
100 § 3.3.7.38
Applying
reload, no restart needed
Queue
will not touch what is already queued
Where to write it
<source>
How many times
once in the block
Everything here comes from the manual, with the section.
Inheritance
The value is merged across matching <source> blocks, the narrow entry overriding the wide one. Above the submission block there is nothing to inherit from — the directive is not described at node level. It is picked up by pmta reload.
How to check on the node
No node command prints this setting — look at the configuration, together with the neighboring line:
grep -n 'max-message-hops\|add-received-header' /etc/pmta/config
pmta test config
If add-received-header is false, the limit remains a formality for loops through this node. The triggering itself is visible in the log at reception — the message is rejected without getting into the queue.
In the Hub
Cost of a mistake
reversible: harms only while it is set
Scenarios
Untangle a stuck queue