Zum Hauptinhalt springen
Ridvan Ereng
Full Stack & AI Engineer
Alle Beiträge

Email-Deliverability für SaaS: Was die Tutorials weglassen

SPF, DKIM, DMARC sind nur die halbe Miete. Hier ist, was wirklich darüber entscheidet, ob deine Mails im Postfach landen oder im Spam-Ordner verschimmeln.

4 min Lesezeit

Es gibt unendlich viele Blog-Posts zu SPF, DKIM und DMARC. Sie erklären die Records, du fügst sie ein, und dann landen deine Mails trotzdem im Promotions-Tab. Oder schlimmer, im Spam-Ordner.

Bei einem Outreach-Tool, das ich an einem Side-Project getestet habe, lag die Deliverability bei den ersten Tests bei 38%. Nach drei Wochen Hardening waren wir bei 92%. Hier ist, was die Differenz gemacht hat.

Die unterschätzte Größe: IP-Reputation#

Wer einen MX-Eintrag bei einem Hyperscaler hat (AWS SES, SendGrid, Mailgun), teilt sich eine IP-Range mit tausenden anderen Sendern. Wenn die anderen Sender Spam verschicken, bekommst du das ab. Das ist keine Theorie. Bei einem Test mit Standard-SES-Pool fielen 23% meiner Mails in einen sekundären Bucket, obwohl alle Authentifizierungs-Records sauber waren.

Lösung: dedizierte IPs ab Volumen. SES bietet das ab ungefähr 100.000 Mails/Monat. Bis dahin: andere Provider. Wer es mit weniger Volumen sauber will, nimmt einen kleineren Anbieter mit besserer Pool-Hygiene (Postmark, Hetzner Mailservice) oder hostet sich selbst.

Preflight-Validation als Quality-Gate#

Bevor eine Kampagne startet, prüfe ich technisch alles, was später Bounce-Rate verursachen könnte. Konkret:

  1. Email-Validation pro Empfänger. NeverBounce oder ZeroBounce machen das batchweise für unter 0,5 Cent pro Adresse. Eine Liste, die 12% ungültige Mails enthält, killt deine Reputation in einem einzigen Send.
  2. MX-Record-Existenz. Wenn der MX-Server der Empfänger-Domain nicht antwortet, ist die Mail technisch zustellbar, aber praktisch tot. Bei B2B-Listen aus Apollo oder Hunter habe ich regelmäßig 4-6% solcher Adressen.
  3. SPF/DKIM/DMARC Reverse-Check für die eigene Domain. Es reicht nicht, dass die Records existieren. Sie müssen auch das tatsächlich verwendete Send-Setup decken.

In Code sieht das ungefähr so aus:

async function preflightCheck(emails: string[]): Promise<PreflightReport> {
  const validation = await neverBounce.batch(emails);
  const mxChecks = await Promise.allSettled(
    [...new Set(emails.map(e => e.split("@")[1]))].map(checkMx)
  );
  const ownDomain = await checkOwnDomainAuth();
 
  const grade = calculateGrade({
    invalidCount: validation.results.filter(r => r.result !== "valid").length,
    mxFailures: mxChecks.filter(c => c.status === "rejected").length,
    ownDomainOk: ownDomain.spf && ownDomain.dkim && ownDomain.dmarc,
  });
 
  return { grade, validation, mxChecks, ownDomain };
}

Das Ergebnis ist ein A-bis-F-Grade, das die Kampagne erst freigibt, wenn alles auf B oder besser steht. Klingt rigide, hat aber bei den letzten drei Testkampagnen die Bounce-Rate von 7% auf unter 1,5% gedrückt.

RFC 8058: Der ignorierte Standard#

Die meisten Outreach-Tools generieren Unsubscribe-Links über einen einfachen Token. RFC 8058 spezifiziert seit 2017 einen besseren Weg: List-Unsubscribe-Header und das One-Click-Verhalten für Postfächer wie Gmail.

List-Unsubscribe: <https://example.com/unsub?token=abc>, <mailto:unsub@example.com>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

Gmail rendert den Unsubscribe-Button direkt im Header, sobald diese Records vorhanden sind. Was viele übersehen: Gmail erwartet, dass der Unsubscribe-Endpoint den User auf einen POST direkt austrägt, ohne Confirmation-Page. Wer hier eine Form mit „bist du sicher?" zeigt, verliert wahrscheinlich Postfach-Vertrauen.

Beim eigenen Setup verwende ich HMAC-signierte Tokens:

function generateUnsubscribeUrl(leadId: string, secret: string): string {
  const payload = `${leadId}.${Date.now()}`;
  const signature = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("base64url");
  return `https://my.app/unsub?p=${payload}&s=${signature}`;
}

Das verhindert, dass jemand Tokens errät und fremde Empfänger austrägt. Fünf Minuten Aufwand, eliminiert eine ganze Klasse von Angriffen.

Auto-Pause bei Bounce-Rate über 1%#

Die hartnäckigste Lektion war diese: ich muss meine Sends auto-pausieren, sobald die Bounce-Rate-Trendlinie über 1% kippt.

Was zuerst passiert ist: ich hatte eine Kampagne mit 8.000 Empfängern, die ersten 500 Sends gingen sauber raus. Bounce-Rate bei 0,4%. Ab Stunde drei stieg sie auf 2,8%, weil ein paar Listenfetzen aus einem alten Provider nicht aussortiert waren. Bis ich es gemerkt habe, waren 2.000 Mails raus und meine IP-Reputation für die Woche im Keller.

Heute läuft das so: nach jeden 100 gesendeten Mails berechne ich die rolling Bounce-Rate. Bei über 1,5% wird der Send-Cron automatisch gestoppt, ich bekomme einen Slack-Alert. Der Cron lässt sich erst manuell wieder anwerfen, nachdem ich die Liste neu validiert habe.

async function shouldContinueSending(campaignId: string): Promise<boolean> {
  const last100 = await db.sends.findMany({
    where: { campaignId },
    orderBy: { sentAt: "desc" },
    take: 100,
  });
  const bounces = last100.filter(s => s.status === "bounced").length;
  const rate = bounces / Math.max(last100.length, 1);
 
  if (rate > 0.015) {
    await pauseCampaign(campaignId, `bounce-rate ${(rate * 100).toFixed(1)}%`);
    return false;
  }
  return true;
}

Was ich noch lerne#

DKIM-Key-Rotation. Die meisten Tutorials sagen „leg deinen Schlüssel an und vergiss ihn". Echte Domains rotieren ihre DKIM-Keys alle 6-12 Monate, mit Selector-Wechsel (s2025-1, s2025-2). Setze ich gerade als Cron-Job um, mehr dazu wenn das stabil läuft.

Die andere offene Baustelle ist Subject-Line-Klassifikation. Manche Phrasen triggern Spam-Filter härter als andere („Last Chance", „FREE Trial"), und es gibt keine offizielle Liste. Was funktioniert ist eine Pre-Send-Prüfung mit einem kleinen Modell (Haiku reicht), das auf einer Liste verbrannter Phrasen trainiert ist.

Was du heute tun kannst#

Wenn du eine SaaS-Anwendung baust, die irgendetwas mit Email zu tun hat: implementiere die drei Sachen aus diesem Post in dieser Reihenfolge.

  1. Preflight-Validation vor jedem Send.
  2. RFC-8058-konforme Unsubscribe-Header.
  3. Auto-Pause bei Bounce-Rate-Anomalien.

Das sind ungefähr zwei Tage Arbeit und sie machen den Unterschied zwischen „landet im Postfach" und „landet bei niemandem".

Wenn du dabei tieferen Support brauchst, bei mir gibt es einen 15-min Call ohne Strings. Wir schauen auf dein Setup und ich sage ehrlich, wo die größten Hebel liegen.