Skip to content
academia.sh

Lesson 10 / 16

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close