Skip to main content

Email Deliverability and Sender Reputation: Measuring, Warming, and Defending It

Measure and defend transactional sender reputation: IP and domain scoring, warm-up ramps, Postmaster Tools signals, blocklist removal, and seed-list placement checks.

Every transactional message you send is scored before it is shown. Authentication decides whether a receiver believes the message came from you; reputation decides whether it wants to accept mail from you at all. This section covers the second half — the continuously recomputed opinion Gmail, Yahoo and Microsoft hold about your sending identity, how to measure it with the signals those providers actually publish, and how to recover when it degrades.

The distinction matters because the failure modes are completely different. An authentication problem is deterministic: a record is wrong, every message fails, and fixing the record fixes every message at once. A reputation problem is statistical and lagging: nothing is misconfigured, messages still authenticate cleanly, and yet an increasing share of them land in spam. You cannot debug the second with the tools you use for the first, which is why teams that have already got SPF, DKIM and DMARC alignment correct are often still surprised when placement collapses.

Reputation Is Two Scores, Not One

Mailbox providers maintain reputation along two independent axes, and confusing them leads to the wrong remediation.

IP reputation attaches to the address that opened the SMTP connection. It is the older of the two signals and still dominant at Microsoft. On a shared pool your score is the pool's score, which means a neighbour's bad campaign can depress your placement and there is nothing you can do about it except leave. On a dedicated IP the score is entirely yours, but it starts at nothing and has to be built by sending consistent volume — which is what a warm-up ramp is for.

Domain reputation attaches to the domain in the From header and the d= tag of your DKIM signature. Gmail weighs it far more heavily than IP reputation, and unlike an IP it follows you across provider migrations. That portability is the point: you can change ESPs, change IPs, and keep the reputation you built, provided the signing domain stays the same. It is also why mixing traffic types on one domain is so damaging — a marketing send that draws complaints degrades the same score your password resets depend on.

Two independent reputation scores IP reputation attaches to the sending address and is lost on migration, while domain reputation attaches to the From and DKIM signing domain and travels with you. Two Scores, Two Lifetimes IP reputation attaches to the connecting IP address Shared pool: you inherit the neighbours. Dedicated: yours alone, but starts cold. Dominant signal at Microsoft. lost the moment you change providers Domain reputation attaches to the From and DKIM d= domain Built from complaint rate, engagement and how many messages you send to addresses that no longer exist. survives migration — the asset worth building Diagnose which score moved before changing anything: the remediations have almost nothing in common.
IP reputation is rented and resets on migration; domain reputation is owned, which is why it should carry the weight of your strategy.

The practical consequence is a subdomain strategy. Send transactional mail from a dedicated subdomain (mail.example.com), marketing from another (news.example.com), and internal notifications from a third. Each accrues its own domain reputation, and a bad month on one does not follow the others into the inbox. Publish DMARC at the organisational domain so the policy covers all of them, and align each subdomain's DKIM signature to itself.

The Signals Providers Actually Publish

You cannot see your reputation score. What you can see is a small set of published telemetry, and the discipline is to treat those numbers as the ground truth rather than your own delivery logs — which only tell you the receiver said 250 OK, not where the message was filed.

Signal Source What it tells you Action threshold
Domain reputation band Google Postmaster Tools Gmail's own bucketed view: bad, low, medium, high Anything below high warrants investigation
User-reported spam rate Google Postmaster Tools Complaints as a share of delivered mail Below 0.10%; Gmail's stated ceiling is 0.30%
Authentication pass rates Google Postmaster Tools SPF, DKIM and DMARC pass percentage over time Any sustained drop below 99%
Complaint and trap hits Microsoft SNDS Per-IP complaint rate and spam-trap counts for Outlook.com Any trap hit at all
Blocklist status Spamhaus, SURBL, Barracuda Whether the IP or a linked domain is listed Any listing is an incident
Inbox placement Seed-list test Where mail actually lands across a panel of accounts Inbox share below 90%

Two of these deserve emphasis. The user-reported spam rate is the single most actionable number Gmail gives you: it is a direct measurement of recipients pressing the spam button, and it moves before the reputation band does. Treat it as a leading indicator and the band as a lagging one. Spam-trap hits are categorically different from complaints — a trap is an address that cannot have opted in, so a hit means your list acquisition or hygiene is broken, not that your content is unpopular.

Complaint rate leads the reputation band A weekly series where the complaint rate rises in week three but the published reputation band only drops in week five, and recovery lags by a similar margin. The Band Moves Weeks After the Complaints Do Bar height is the weekly complaint rate; the strip beneath is the published reputation band high high medium medium wk 1 wk 3 — complaints spike wk 5 — band drops wk 8 Acting on the complaint rate buys you weeks; waiting for the band to drop means the damage is already priced in.
Complaint rate is the leading indicator and the reputation band the lagging one — alerting on the former is what makes recovery cheap.

Core Implementation: A Reputation Monitor That Alerts Before Placement Drops

Postmaster Tools exposes its data through an API, which means the numbers above belong in your monitoring stack rather than in a dashboard someone remembers to check. The pattern below pulls the daily traffic statistics for a domain, evaluates them against thresholds, and raises an alert on the leading indicators.

# reputation_monitor.py — pull Gmail's published telemetry and alert on the
# leading indicators before inbox placement visibly degrades.
import datetime as dt
from dataclasses import dataclass

from googleapiclient.discovery import build          # google-api-python-client
from google.oauth2 import service_account

SCOPES = ["https://www.googleapis.com/auth/postmaster.readonly"]

# Gmail's stated ceiling is 0.30%; alert far below it so there is time to react.
SPAM_RATE_WARN = 0.0010      # 0.10% — investigate
SPAM_RATE_PAGE = 0.0020      # 0.20% — stop the offending stream now
AUTH_PASS_FLOOR = 0.99       # any sustained dip means a signing or record problem
BAD_BANDS = {"BAD", "LOW"}   # Gmail returns BAD | LOW | MEDIUM | HIGH


@dataclass
class Reading:
    date: dt.date
    spam_rate: float
    domain_band: str
    spf_pass: float
    dkim_pass: float
    dmarc_pass: float


def fetch(domain: str, key_file: str, days: int = 7) -> list[Reading]:
    creds = service_account.Credentials.from_service_account_file(key_file, scopes=SCOPES)
    api = build("gmailpostmastertools", "v1beta1", credentials=creds)
    parent = f"domains/{domain}"

    readings = []
    # The API returns one resource per day; the most recent day is usually T-2.
    for row in api.domains().trafficStats().list(
        parent=parent, pageSize=days
    ).execute().get("trafficStats", []):
        # Resource names look like domains/mail.example.com/trafficStats/20260718
        stamp = row["name"].rsplit("/", 1)[-1]
        readings.append(Reading(
            date=dt.datetime.strptime(stamp, "%Y%m%d").date(),
            spam_rate=float(row.get("userReportedSpamRatio", 0.0)),
            domain_band=row.get("domainReputation", "UNKNOWN"),
            spf_pass=float(row.get("spfSuccessRatio", 1.0)),
            dkim_pass=float(row.get("dkimSuccessRatio", 1.0)),
            dmarc_pass=float(row.get("dmarcSuccessRatio", 1.0)),
        ))
    return sorted(readings, key=lambda r: r.date)


def evaluate(readings: list[Reading]) -> list[tuple[str, str]]:
    """Return (severity, message) pairs. Severity is 'page' or 'warn'."""
    alerts = []
    if not readings:
        return [("warn", "no Postmaster Tools data returned — check API access")]

    latest = readings[-1]

    if latest.spam_rate >= SPAM_RATE_PAGE:
        alerts.append(("page",
            f"spam rate {latest.spam_rate:.3%} on {latest.date} — pause the newest stream"))
    elif latest.spam_rate >= SPAM_RATE_WARN:
        alerts.append(("warn",
            f"spam rate {latest.spam_rate:.3%} on {latest.date} — investigate today"))

    if latest.domain_band in BAD_BANDS:
        alerts.append(("page", f"domain reputation is {latest.domain_band}"))

    for label, value in (("SPF", latest.spf_pass),
                         ("DKIM", latest.dkim_pass),
                         ("DMARC", latest.dmarc_pass)):
        if value < AUTH_PASS_FLOOR:
            # An authentication dip is deterministic — a record or selector broke.
            alerts.append(("page", f"{label} pass rate {value:.2%} — check DNS and signing"))

    # A doubling week over week matters even below the absolute threshold.
    if len(readings) >= 8:
        prev, curr = readings[-8].spam_rate, latest.spam_rate
        if prev > 0 and curr > prev * 2 and curr >= 0.0005:
            alerts.append(("warn", f"spam rate doubled week over week: {prev:.3%}{curr:.3%}"))

    return alerts

Two design decisions in that code are deliberate. First, the relative check at the end fires on a doubling even while the absolute rate is still healthy — reputation damage compounds, and the cheapest moment to intervene is before you cross a published threshold. Second, an authentication dip is paged, not warned: unlike a reputation slide, it has a specific cause and a same-day fix, and it usually means a DKIM selector was rotated or an SPF include changed underneath you.

Second Pattern: Seed-List Placement Testing

Postmaster Tools tells you what Gmail thinks. It does not tell you where the message landed, and it says nothing about Microsoft, Yahoo or the smaller providers your customers actually use. Seed-list testing fills that gap: you send the real production message to a panel of accounts you control across providers, then read back which folder each copy landed in.

// placement-check.mjs — send the real template to a seed panel, then report
// per-provider placement. Run on a schedule, not only at release time.
import { renderTemplate } from "./render.mjs";
import { send } from "./esp/index.mjs";
import { readPlacement } from "./seedbox.mjs";

// One seed address per provider, on accounts you own. Keep the panel small and
// stable — comparability over time matters more than breadth.
const PANEL = [
  { provider: "gmail",       address: "seed-gmail@example.net" },
  { provider: "outlook.com", address: "seed-outlook@example.net" },
  { provider: "yahoo",       address: "seed-yahoo@example.net" },
  { provider: "apple",       address: "seed-icloud@example.net" },
  { provider: "corporate",   address: "seed-o365@example.net" },
];

const INBOX_FLOOR = 0.9;   // below this, treat it as an incident

export async function runPlacementCheck(templateId) {
  // Send the SAME rendered bytes to every seed, so a difference in outcome is a
  // difference in receiver behaviour rather than in content.
  const { html, text, subject } = await renderTemplate(templateId, {
    // Fixed fixture data — varying content between runs makes results unreadable.
    customer: { name: "Seed Panel", email: "seed@example.net" },
    order: { id: "SEED-0001", total: "£42.00" },
  });

  const sent = await Promise.all(
    PANEL.map(({ provider, address }) =>
      send({ to: address, subject, html, text, tags: { placement: templateId } })
        .then((res) => ({ provider, address, messageId: res.messageId }))
        .catch((err) => ({ provider, address, error: String(err) })),
    ),
  );

  // Receivers file mail asynchronously; poll rather than reading immediately.
  const results = await readPlacement(sent, { timeoutMs: 15 * 60_000 });

  const inbox = results.filter((r) => r.folder === "inbox").length;
  const share = inbox / results.length;

  return {
    templateId,
    share,
    ok: share >= INBOX_FLOOR,
    // Per-provider detail is what makes the result actionable: "Outlook.com is
    // filtering us" is a different investigation from "everyone is".
    byProvider: Object.fromEntries(results.map((r) => [r.provider, r.folder])),
  };
}

Seed panels have a known limitation worth stating plainly: seed accounts have no genuine engagement history, so providers that weigh per-recipient engagement will treat them differently from a real subscriber. Placement on a seed panel is therefore a floor rather than a prediction. What it is genuinely good at is detecting change — a template that placed in the inbox last week and in spam this week is a signal you can act on regardless of the absolute number. Run it on a schedule and diff against the previous run, exactly as you would a visual regression baseline.

Provider Constraint Reference

Provider Primary signal Telemetry available Notable behaviour
Gmail Domain reputation Postmaster Tools API and UI Weighs per-recipient engagement; requires DMARC alignment at bulk volume
Yahoo Domain and IP Complaint feedback loop Enforces the same 2024 bulk-sender rules as Gmail
Outlook.com IP reputation SNDS, per-IP Trap hits are heavily weighted; delisting requires a support request
Microsoft 365 Tenant policy plus IP Message trace, tenant-side Corporate filters can override sender reputation entirely
Apple Mail (iCloud) Opaque None published Mail Privacy Protection makes open rates useless as a health signal
Samsung Email Delegated to the account provider None Reputation is inherited from the underlying mailbox provider

The row that surprises people is Apple. Mail Privacy Protection pre-fetches images for every message, which means open rate is no longer a usable engagement metric for a meaningful slice of your audience. If your reputation monitoring or your re-engagement logic keys on opens, it is reading noise. Use click-through and, for transactional mail, the downstream action the message exists to trigger — a completed password reset, a viewed receipt.

Numbered Pipeline: Wiring Reputation Into Delivery

  1. Separate the streams. Give transactional mail its own subdomain with its own DKIM selector, so its reputation is measured independently of marketing traffic.
  2. Verify the domain in Postmaster Tools by publishing the TXT record, and grant a service account read access so the monitor above can authenticate without a human in the loop.
  3. Ingest daily. Schedule the monitor to run once a day after the T-2 data lands, and store every reading — the historical series is what makes a trend visible.
  4. Alert on leading indicators: spam rate crossing 0.10%, a week-over-week doubling, or any authentication pass rate below 99%.
  5. Schedule the seed panel to run against the top three templates by volume, and diff placement against the previous run rather than against an absolute target.
  6. Feed suppression back in. Every hard bounce and complaint must reach the suppression store through the webhook event pipeline, because continuing to send to dead addresses is the fastest way to lose a reputation band.
  7. Gate risky changes. A change to the sending domain, the DKIM selector, the ESP or the IP pool should trigger a placement check before it reaches full volume — the same staged approach used when migrating between providers.

Debugging: Named Symptoms, Cause, and Fix

Symptom: Postmaster Tools shows no data at all. Cause: either the domain verification TXT record is missing, or you are below Gmail's reporting volume floor of roughly 100 messages a day to Gmail addresses. Fix: confirm the TXT record resolves, then check that the domain you verified is the DKIM d= domain — verifying the organisational domain shows nothing when you sign as a subdomain.

Symptom: spam rate spikes on one specific day and returns to normal. Cause: almost always a single large send to a stale segment, not a content change. Fix: correlate the spike date against your send log by template. A one-day spike from a re-engagement or import batch is a list-hygiene problem; suppress the segment and let the rolling average recover.

Symptom: authentication pass rate drops to roughly 50%. Cause: a second sending source that is not aligned — typically a new vendor sending as your domain, or a DKIM key rotation where the old selector was removed before the new one propagated. Fix: read the DMARC aggregate reports to identify the unaligned source, and when rotating keys, publish the new selector and sign with it before withdrawing the old one.

Symptom: Gmail placement is fine but Outlook.com sends everything to Junk. Cause: IP reputation at Microsoft, which weighs it far more heavily than Gmail does, often driven by a spam-trap hit. Fix: check SNDS for the sending IP, and if it shows trap hits, audit list acquisition rather than content. Microsoft delisting requires a support submission; content changes will not clear it.

Symptom: placement degrades gradually over months with no incident. Cause: list decay. Addresses that stop existing become spam traps over time, and continuing to mail them converts a good list into a bad one. Fix: enforce suppression on every hard bounce, and stop sending to addresses that have not engaged with anything for an extended period — even in transactional streams, where the account itself may be abandoned.

Validation and Deployment Checklist

Recovering From a Blocklist Listing

A blocklist entry is the one reputation event that behaves like an outage rather than a slide: placement does not degrade gradually, it stops. Because the major lists are consulted by thousands of receivers, a single listing on Spamhaus can take a working sending IP from full delivery to near-total rejection within minutes, and the bounce messages will name the list explicitly.

The response has a fixed order, and the common mistake is starting at the end of it. First, find the cause. A listing is a symptom, and delisting without remediation results in an immediate relisting plus a worse standing with the list operator. The three causes that account for most transactional listings are a compromised credential being used to relay spam, a large import of purchased or scraped addresses that hit spam traps, and a misconfigured application looping on a send. Each leaves a distinctive trace: a credential compromise shows unfamiliar template IDs or sending patterns, a bad import shows a volume spike to previously unseen domains, and a loop shows the same recipient repeatedly.

Second, stop the sending. Continuing to send while listed adds evidence against you and, if the cause is still active, deepens the problem. Pause the affected stream at the queue rather than at the application, so nothing is lost while you investigate — this is one of the situations the queueing and retry layer exists for.

Third, remediate visibly. Rotate the credential, purge the imported segment, fix the loop — and record what you did, because most delisting forms ask. Operators are looking for evidence that the cause is gone, not for an assurance that it will not recur.

Only then request delisting. Most lists offer self-service removal with a cooling-off period; some, including Microsoft's internal lists, require a support ticket and a human review. Expect the first removal to be granted quickly and any subsequent one for the same cause to be much slower.

Blocklist response sequence Four ordered steps: identify the cause, pause the affected stream, remediate and record the action, then request delisting. Delisting Is the Last Step, Not the First 1 — find the cause credential, import, or a send loop 2 — pause the stream at the queue, so nothing is lost 3 — remediate rotate, purge, fix — and write it down 4 — request delisting with the evidence Requesting removal before remediating usually results in a relisting, and repeat listings are granted removal far more slowly. Domain listings are worse than IP listings: changing IPs does not escape them.
The order matters because list operators grade on evidence of remediation, and a repeat listing for the same cause is treated far less generously.

One asymmetry is worth internalising. An IP listing can, in the worst case, be escaped by moving to a different address — an expensive and reputation-resetting move, but possible. A domain listing cannot: the domain appears in the message body and the From header, so no amount of infrastructure change avoids it. That asymmetry is the strongest practical argument for keeping marketing and transactional traffic on separate subdomains, since a listing earned by one does not automatically ground the other.

Frequently Asked Questions

Should transactional email use a dedicated IP?

Only above roughly 50,000 messages a month to a single provider. Below that, a dedicated IP never accumulates enough consistent volume for receivers to form an opinion, so it stays effectively cold and performs worse than a well-run shared pool. The threshold is about volume consistency, not just total: 50,000 messages spread evenly beats 200,000 sent in two bursts, because a quiet IP that suddenly sends is the exact pattern receivers treat as suspicious.

How long does it take to recover a damaged reputation?

Weeks, and the shape of the recovery is not symmetrical with the damage. Reputation scores are computed over rolling windows, so a single bad day continues to weigh on the average until it ages out. Assume two to four weeks of clean sending at reduced volume before a band improves, and expect no improvement at all if the underlying cause — a stale segment, an unsuppressed bounce path — has not been fixed first.

Does message content still matter if authentication and reputation are healthy?

Yes, but much less than most teams assume for transactional mail. Content-based filtering is a secondary signal that mostly matters when reputation is neutral or unknown. A sender with a high domain reputation can send fairly spam-like content and still reach the inbox; a sender with a bad reputation cannot rescue placement by rewording a subject line. Fix reputation first, then tune content — the spam filter and placement testing workflow covers the content half.

Why do our own delivery logs show 100% delivered while placement is clearly bad?

Because your logs record what the receiving server said at SMTP time, and receivers accept mail they intend to file in spam. A 250 OK means the message was accepted for delivery, not that a human will see it. This gap is precisely why Postmaster Tools and seed-list testing exist — they are the only view into what happened after acceptance.

Should marketing and transactional mail share a sending domain?

No. Marketing traffic draws complaints at rates that transactional mail never does, and on a shared domain those complaints degrade the reputation your password resets depend on. Separate subdomains isolate the blast radius and let you diagnose each stream independently. The one cost is that each subdomain builds reputation on its own, so a new subdomain needs its own warm-up.

What is the single most important metric to watch?

The user-reported spam rate from Postmaster Tools. It is a direct measurement rather than an inference, it moves earlier than the reputation band, and it maps cleanly to an action — find the stream that caused the spike and stop it. Everything else on the dashboard is either lagging, aggregated too coarsely to act on, or measuring authentication rather than reputation.


← Back to Transactional Email Delivery Infrastructure