---
title: 'Mail Delivery Issues'
source: 'https://academia.sh/en/courses/servers-and-hardening/mail-delivery-issues'
course: 'Servers, Networking and Hardening'
language: en
updated: '2026-09-13T05:08:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows how reputation drops by counting the factors that degrade deliverability separately'
    - 'Derives the count of delayed and never-arriving messages, and the recovery time, in a one-period run'
    - 'Separates by name the part the sending side cannot measure, and states the remaining uncertainty'
---

# Mail Delivery Issues

Modeling deliverability as a ratio: counting bounced messages, complaints, sudden volume spikes, and missing verification records separately, and measuring how many periods blacklisting and recovery take.

The previous three lessons established, at the record level, where the name points, who can
send messages in that name's name, and what happens to a noncompliant message. All three are
measures of the **sending side**. Where this lesson starts, messages are now compliant,
signed, and the policy is declared — and they may still not arrive.

The receiving side does not accept a message by looking only at the records. It also looks at
the sender's **history**: how many of its messages bounced in the recent period, how many
people complained, whether volume spiked suddenly. The summary of this history is called
**reputation**, and it behaves like a number: invisible while high, and it determines
everything once it drops. The regional measurement network's work-order notifications to
field teams fall under this measure, and the network is fictional.

## Deliverability Is a Ratio

When a message is sent, one of three outcomes happens: it reaches the inbox, it gets delayed
and delivered later, or it never arrives. **Deliverability** is the ratio of these three
outcomes — not a yes/no.

The factors that degrade the ratio can be counted separately, and all four pull in the same
direction:

- **Bounce rate.** Sending to an address that no longer exists shows the list is not
  maintained.
- **Complaint rate.** A recipient marking a message as unwanted is the heaviest single factor.
- **Sudden volume spike.** Volume from the same address jumping to several times its usual
  level is a deviation from established behavior.
- **Missing verification record.** If the previous two lessons' records are missing, it does
  not produce an incident by itself; it only lowers the highest reputation reachable.

The last item is the link connecting this topic: when sender verification is not set up, the
resulting harm does not look like an incident — it behaves like a **ceiling**.

## A One-Period Run

**DR19.** A period is one week and twenty periods are run; volume, rates, and thresholds are
fictional.
**DR20.** Reputation is modeled as a single number.
**DR21.** Reputation falls fast and climbs slowly: it drops by at most 35 points in one period
and rises by at most 5.
**DR22.** Blacklisting happens below 35 points; exiting requires two periods above 55 points.

```javascript
// Model: deliverability is a ratio, determined via reputation. Periods,
// factors, and thresholds are fictional; not a real service's measurement.
const STARTING_REPUTATION = 88;
const BLACKLIST_ENTRY = 35;   // the sending address is blacklisted below this value
const BLACKLIST_EXIT = 55;    // two periods are waited above this value

// Period inputs: volume, bounce rate, complaint rate, whether the verification record is missing
const PERIODS = [
  { d: 1,  volume: 400,  bounceRate: 0.012, complaintRate: 0.0004, recordsMissing: true },
  { d: 2,  volume: 410,  bounceRate: 0.011, complaintRate: 0.0005, recordsMissing: true },
  { d: 3,  volume: 395,  bounceRate: 0.013, complaintRate: 0.0004, recordsMissing: true },
  { d: 4,  volume: 420,  bounceRate: 0.012, complaintRate: 0.0006, recordsMissing: true },
  { d: 5,  volume: 405,  bounceRate: 0.010, complaintRate: 0.0004, recordsMissing: true },
  { d: 6,  volume: 3200, bounceRate: 0.140, complaintRate: 0.0040, recordsMissing: true },   // legacy list added
  { d: 7,  volume: 3100, bounceRate: 0.135, complaintRate: 0.0052, recordsMissing: true },
  { d: 8,  volume: 2900, bounceRate: 0.120, complaintRate: 0.0048, recordsMissing: true },
  { d: 9,  volume: 900,  bounceRate: 0.040, complaintRate: 0.0020, recordsMissing: false },  // list cleaned up
  { d: 10, volume: 450,  bounceRate: 0.015, complaintRate: 0.0008, recordsMissing: false },
  { d: 11, volume: 430,  bounceRate: 0.011, complaintRate: 0.0005, recordsMissing: false },
  { d: 12, volume: 425,  bounceRate: 0.009, complaintRate: 0.0004, recordsMissing: false },
  { d: 13, volume: 415,  bounceRate: 0.008, complaintRate: 0.0003, recordsMissing: false },
  { d: 14, volume: 420,  bounceRate: 0.008, complaintRate: 0.0003, recordsMissing: false },
  { d: 15, volume: 430,  bounceRate: 0.007, complaintRate: 0.0002, recordsMissing: false },
  { d: 16, volume: 425,  bounceRate: 0.007, complaintRate: 0.0002, recordsMissing: false },
  { d: 17, volume: 440,  bounceRate: 0.006, complaintRate: 0.0002, recordsMissing: false },
  { d: 18, volume: 435,  bounceRate: 0.006, complaintRate: 0.0002, recordsMissing: false },
  { d: 19, volume: 430,  bounceRate: 0.005, complaintRate: 0.0001, recordsMissing: false },
  { d: 20, volume: 445,  bounceRate: 0.005, complaintRate: 0.0001, recordsMissing: false },
];

// Bands: by reputation range, [inbox, delayed, undelivered]
const BANDS = [
  [80, [0.98, 0.02, 0.00]],
  [60, [0.88, 0.10, 0.02]],
  [40, [0.65, 0.25, 0.10]],
  [20, [0.35, 0.35, 0.30]],
  [0,  [0.10, 0.30, 0.60]],
];
const BLACKLIST_BAND = [0.05, 0.15, 0.80];

const band = (reputation) => BANDS.find(([threshold]) => reputation >= threshold)[1];
const clamp = (x) => Math.max(0, Math.min(100, x));

const MAX_DROP = 35;      // the most points it can fall in one period
const MAX_RECOVERY = 5;   // the most points it can climb in one period

let reputation = STARTING_REPUTATION;
let blacklisted = false;
let cleanPeriods = 0;
let previous = PERIODS[0].volume;
const history = [];

for (const g of PERIODS) {
  const growth = g.volume / previous;
  let target = 100;
  target -= Math.max(0, g.bounceRate - 0.02) * 300;        // bounce rate
  target -= Math.max(0, g.complaintRate - 0.001) * 5000;   // complaint rate
  target -= growth > 3 ? 20 : 0;                            // sudden volume spike
  target -= g.recordsMissing ? 12 : 0;                      // missing verification record
  target = clamp(target);
  reputation = target > reputation
    ? Math.min(target, reputation + MAX_RECOVERY)
    : Math.max(target, reputation - MAX_DROP);

  if (!blacklisted && reputation < BLACKLIST_ENTRY) { blacklisted = true; cleanPeriods = 0; }
  else if (blacklisted) {
    if (reputation >= BLACKLIST_EXIT) cleanPeriods++; else cleanPeriods = 0;
    if (cleanPeriods >= 2) blacklisted = false;
  }

  const [ik, dk, uk] = blacklisted ? BLACKLIST_BAND : band(reputation);
  history.push({
    ...g, target: Math.round(target), reputation: Math.round(reputation), blacklisted,
    inbox: Math.round(g.volume * ik),
    delayed: Math.round(g.volume * dk),
    undelivered: Math.round(g.volume * uk),
  });
  previous = g.volume;
}

console.log('period  volume  bounce%  complaint%  reputation  black  inbox  delayed  undelivered');
for (const s of history) {
  console.log(
    `d${String(s.d).padStart(2, '0')}`.padEnd(8) + String(s.volume).padEnd(8) +
    (s.bounceRate * 100).toFixed(1).padEnd(9) + (s.complaintRate * 100).toFixed(2).padEnd(12) +
    String(s.reputation).padEnd(12) + (s.blacklisted ? 'yes' : '-').padEnd(7) +
    String(s.inbox).padEnd(7) + String(s.delayed).padEnd(9) + String(s.undelivered),
  );
}

const total = (field) => history.reduce((t, s) => t + s[field], 0);
console.log('\ntotal volume:', total('volume'), '| inbox:', total('inbox'),
  '| delayed:', total('delayed'), '| undelivered:', total('undelivered'));
console.log('delivery rate:', (total('inbox') / total('volume') * 100).toFixed(1) + '%',
  '| loss rate:', (total('undelivered') / total('volume') * 100).toFixed(1) + '%');

const after = history.filter((s) => s.d >= 10);
const sum = (arr, field) => arr.reduce((t, s) => t + s[field], 0);
console.log('after the incident (d10-d20) volume:', sum(after, 'volume'),
  '| delayed:', sum(after, 'delayed'), '| undelivered:', sum(after, 'undelivered'));

const declineStart = history.find((s) => s.reputation < 80);
const bottom = history.reduce((a, b) => (b.reputation < a.reputation ? b : a));
const recoveryPoint = history.find((s) => s.d > bottom.d && s.reputation >= 80);
console.log('\nreputation started falling: d' + declineStart.d, '| bottom:', bottom.reputation, '(d' + bottom.d + ')',
  '| periods spent blacklisted:', history.filter((s) => s.blacklisted).length);
console.log('decline: 1 period | return to 80 threshold: d' + recoveryPoint.d,
  '| recovery:', recoveryPoint.d - bottom.d, 'periods');
```

```
period  volume  bounce%  complaint%  reputation  black  inbox  delayed  undelivered
d01     400     1.2      0.04        88          -      392    8        0
d02     410     1.1      0.05        88          -      402    8        0
d03     395     1.3      0.04        88          -      387    8        0
d04     420     1.2      0.06        88          -      412    8        0
d05     405     1.0      0.04        88          -      397    8        0
d06     3200    14.0     0.40        53          -      2080   800      320
d07     3100    13.5     0.52        33          yes    155    465      2480
d08     2900    12.0     0.48        38          yes    145    435      2320
d09     900     4.0      0.20        43          yes    45     135      720
d10     450     1.5      0.08        48          yes    23     68       360
d11     430     1.1      0.05        53          yes    22     65       344
d12     425     0.9      0.04        58          yes    21     64       340
d13     415     0.8      0.03        63          -      365    42       8
d14     420     0.8      0.03        68          -      370    42       8
d15     430     0.7      0.02        73          -      378    43       9
d16     425     0.7      0.02        78          -      374    43       9
d17     440     0.6      0.02        83          -      431    9        0
d18     435     0.6      0.02        88          -      426    9        0
d19     430     0.5      0.01        93          -      421    9        0
d20     445     0.5      0.01        98          -      436    9        0

total volume: 16875 | inbox: 7682 | delayed: 2278 | undelivered: 6918
delivery rate: 45.5% | loss rate: 41.0%
after the incident (d10-d20) volume: 4745 | delayed: 403 | undelivered: 1078

reputation started falling: d6 | bottom: 33 (d7) | periods spent blacklisted: 6
decline: 1 period | return to 80 threshold: d17 | recovery: 10 periods
```

## Asymmetry

One line carries this entire lesson's weight: **decline takes one period, recovery takes
ten.**

The incident starts at `d06`. A set of addresses left over from years earlier is added to the
field list; volume jumps to eight times its size, the bounce rate spikes to 14%, and
complaints increase. Three factors hit in the same period, and reputation drops from 88 to
53. One period later it falls to 33 and gets **blacklisted**.

What follows from here is no longer about sending behavior. At `d09` the list is cleaned up,
at `d10` volume returns to normal and the rates are good — but reputation can only climb five
points per period. The address stays blacklisted for six periods; after coming off the list,
returning to the 80 threshold takes four more periods. Fixing the mistake took one period;
erasing its consequence took ten. This asymmetry is deliberate and serves the receiving side:
a measure that falls fast stops abuse early, and a measure that climbs slowly requires good
behavior to be sustained for several periods.

The second number shows the price of this. In the eleven periods after the incident — that
is, while sending was entirely normal — **1,078 messages never arrive and 403 messages get
delayed**. These are work-order notifications going to the field team, and there is nothing
wrong with their content; they get dropped purely because of the sender's history. The 41.0%
loss rate in the period total is misleading — most of that loss was already messages that
should not have been sent in the first place. The real cost of this is the normal messages
lost **after** the incident.

## Counting the Factors Separately

Four factors hit the same number, but their contributions are not equal, and the weights in
the model show this.

Bounce rate is the largest single item: at 14%, it alone takes away 36 points. Complaint rate
carries similar weight with a number that looks smaller — five complaints per thousand means
21 points; this is the factor where a small number of receivers' decisions are weighted very
heavily. A sudden volume spike is a fixed 20-point penalty, independent of content: had the
same messages been sent more slowly, this item would never have arisen.

The fourth behaves differently from the others. The missing verification record produces no
incident at all in the first five periods; the delivery ratio is 98%, nothing is visible. All
it does is hold the reputation ceiling at **88**. What this means becomes clear at `d19` and
`d20`: once the record is completed, reputation climbs to 93, then to 98. In other words,
while the record was missing, the system always sat 12 points lower, and when an incident
arrived, it was 12 points closer to the blacklist threshold. Here, the previous two lessons'
records are measured not as a defense but as a **buffer**.

## Separating Flows, and the Surface It Brings

All the loss in this run comes from a single number. Reputation is kept per sender, and the
model has exactly one sender: both the bulk announcement and the work-order notification go
out under the same name. The reputation the bulk list damages also drags down the work order
going to the field team — even though the two flows have no connection to each other.

The countermeasure is separating the flows: bulk and opt-in sending is done from a separate
subdomain, and operational notifications stay under their own name. The separation splits
reputation in two as well; what happens on one side does not drag down the other. The model's
structure says this directly, because the bands derive from reputation, and reputation is
tied to a single sender.

The new surface separation brings is the sum of the first three lessons. Every new subdomain
wants its own set of records: an address record, a sender policy record, a signature record,
its own policy declaration, and its own report address. The number of records in the zone
grows, the chance of a dangling record grows, and the time-to-live decision has to be made
once more. The inventory drift counted in the first lesson, the missing records counted in
the second, and the unprepared flow counted in the third — all three are born from scratch
for the new name. The step that reduces the delivery problem doubles the maintenance surface,
and that cost is part of the decision to separate the name.

## The Part That Cannot Be Measured

**DR23.** The bands convert the reputation range directly into a delivery ratio.
**DR24.** In the model, the sending side sees the count of messages that reach the inbox.

The last assumption does not hold in reality, and it is the part of this topic that cannot be
measured. The sending side knows only two things: whether the message was accepted by the
receiving side, and which messages bounced. Whether an accepted message lands in the inbox or
somewhere else is **invisible**. The "inbox" and "delayed" columns counted separately in the
table above are, in production, a single column: accepted.

Reputation itself is not published either. It can be modeled as a number, but it cannot be
read; every receiving side keeps its own measure, and these measures differ from one another.
Blacklist status can be partly queried, but which threshold the listing decision used, and how
long the exit will take, is unknown from the outside.

Because of this, the only thing that can be measured is **indirect indicators**: bounce rate,
complaint reports, sending to one's own control addresses, and the previous lesson's report
flow. All four show not the outcome itself but the outcome's shadow. When a delivery problem
started is most often learned when a recipient says "the notification is not arriving"; that
is a tip-off, not a measurement.

## Summary

- Deliverability is not a yes/no; it is the ratio of three outcomes: inbox, delay, never
  arriving.
- Four factors can be counted separately; in the model, bounce rate takes away 36 points,
  complaints 21, sudden volume spike 20, and the missing verification record sets a 12-point
  ceiling.
- Decline takes one period, recovery takes ten: reputation drops from 88 to 53 in one period,
  six periods pass on the blacklist, and returning to the 80 threshold takes ten periods after
  the bottom.
- Even after the incident, while sending is entirely normal, 1,078 messages never arrive and
  403 get delayed; these are work-order notifications with nothing wrong in their content.
- Verification records do not prevent an incident, they raise the ceiling; while missing, the
  system sits 12 points closer to the blacklist threshold at all times.
- The part that cannot be measured is the receiver's decision: the sending side sees
  acceptance and rejection, not whether a message lands in the inbox, and reputation is
  published nowhere.

## Next Step

This topic has closed the name. The name now knows where it points, who may send messages in
its name is on record, what happens to a noncompliant message is declared, and where delivery
breaks down can be measured. Every item closed across the four lessons was about **the name**.

What remains is what the name points to. Every address record in the zone file leads to a
machine, and that machine has been sitting in a state never questioned throughout this topic:
reachable from everywhere since the day it was set up. Which ports are listening, where
administrative access is open from, and how many credentials with no expiry sit on it were
never counted. The next topic shifts the scale from the name to the machine and asks the same
question there: how many items does the default setup leave open?
