Overview

I built a transactional email system for a small SaaS two years ago. The emails went out. Some arrived. Some went to spam. Some disappeared entirely. I spent three weeks figuring out why, and the answer was three DNS records I'd never heard of.

This is the setup that took deliverability from "maybe" to "yes, reliably" — with the reasoning behind each piece.

The problem these records solve

SMTP was designed in 1982, when the internet was a research network and nobody lied about who they were. The protocol has no built-in authentication. Anyone can connect to a mail server and claim to be from anyone.

Spam filters evolved to compensate, and the modern answer is three DNS records that together prove a message is legitimate:

RecordWhat it provesWhere it lives
SPFWhich servers are allowed to send for your domainTXT record
DKIMThat the message hasn't been modified in transitTXT record + signature header
DMARCWhat to do when SPF and DKIM fail, and where to send reportsTXT record

None of these is optional anymore. Gmail, Outlook, and Yahoo have been enforcing them since early 2024 for bulk senders. Even for low-volume senders, having them improves the odds of landing in the inbox dramatically.

SPF: the sender allowlist

SPF is a TXT record that lists IP addresses and hosts allowed to send mail for your domain.

example.com.  TXT  "v=spf1 include:_spf.google.com include:sendgrid.net -all"

The mechanism:

  1. Receiving server looks up the SPF record for the domain in the MAIL FROM header (the envelope sender, not the visible From address).
  2. It compares the connecting IP against the list.
  3. It returns a result: pass, fail, softfail, or neutral.

The qualifier on each mechanism tells the receiver how to treat a match or mismatch:

QualifierMeaningTypical use
+PassDefault, explicitly allows
-Fail"This is definitely not us" — the -all at the end
~SoftFail"Probably not us, but not sure"
?Neutral"No opinion" — usually a mistake

Use -all if you're confident about your senders. Use ~all during a transition. Never use ?all — it tells receivers nothing and looks like you don't know what you're doing.

The lookup limit problem

SPF has a hard limit of 10 DNS lookups per check. Every include: counts as one, and some providers nest includes internally. A few integrations in, you exceed 10 and receivers return permerror, which means "SPF failed" regardless of whether you're legitimate.

Check your count with a tool like Kitterman's SPF validator. If you're over, the fix is usually to consolidate providers or use SPF flattening — resolving the includes to IP addresses and listing them directly. Flattening works, but it needs to be re-run when provider IPs change, which they do.

DKIM: cryptographic signature

SPF checks the envelope sender and the IP. DKIM signs the message itself, including headers, so any modification in transit invalidates the signature.

The setup: your mail provider generates a keypair and gives you a public key to add as a DNS TXT record. The private key stays with the provider, which signs outgoing messages with a header like:

DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=selector1;
    h=from:to:subject:date;
    bh=...; b=...

The receiving server looks up the public key at selector1._domainkey.example.com, verifies the signature, and knows the message wasn't modified after signing.

The s=selector1 part is the selector — a named key. This lets you have multiple DKIM keys active, so you can rotate them without downtime.

Common DKIM mistakes

MistakeResult
Wrong selector nameDNS lookup fails, DKIM fails silently
Public key truncated by DNSSignature verification fails
Missing v=DKIM1 in the recordSome receivers reject the record
Mailing list modifying subject linesDKIM invalidated after signing

The mailing list one is a real problem. If someone forwards through a list that prepends [list-name] to the subject, and the subject is in the DKIM signature, the signature breaks. You can't fix this from your side. That's what DMARC's relaxed mode is for.

DMARC: the policy layer

SPF and DKIM are mechanisms. DMARC tells receivers what to do when they fail, and where to send reports.

_dmarc.example.com.  TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; ruf=mailto:dmarc@example.com; pct=100; adkim=r; aspf=r"
TagMeaning
p=noneTake no action, just report — good for the first two weeks
p=quarantineSend failures to spam
p=rejectReject failures outright — the goal
ruaAggregate report address (daily summaries)
rufForensic report address (per-message failures; many providers ignore this)
pctPercentage of messages to which the policy applies
adkim, aspfr for relaxed (allow subdomain match), s for strict

The rollout sequence

  1. Start with p=none and a rua address. Wait two weeks. Reports arrive daily, listing every source sending mail on your behalf.
  2. Read the reports. You will find sources you didn't know about — a legacy application, a marketing tool, a form on a WordPress site. All of them need SPF or DKIM alignment.
  3. Move to p=quarantine; pct=25. Watch reports for a week. If your legitimate mail still passes, increase to pct=50, then pct=100.
  4. Move to p=reject. This is where you want to be. At this point, anyone spoofing your domain will fail outright.

Skipping straight to p=reject will break your mail. The reports exist precisely so you don't have to guess.

Reading DMARC reports without going insane

The reports are XML files, compressed, and they arrive daily from every major provider. Reading them by hand is not an option.

Options:

  • Free service: Postmark's DMARC Digests, or Valimail. Emails you a summary.
  • Self-hosted: parsedmarc, which parses reports into a Database and can forward to Elasticsearch or Grafana.
  • Do nothing: Skip the rua tag. You'll get p=none with no visibility, which defeats the purpose.

I used parsedmarc for a while and then switched to Postmark's digest, because I was spending more time maintaining the parser than reading reports.

Alignment: the part people miss

SPF and DKIM don't need to pass — they need to pass and be aligned with the visible From domain. If your envelope sender is bounce@sendgrid.net but your From header says hello@yourdomain.com, SPF passes for sendgrid but doesn't align with your domain.

DKIM is usually the answer here. Most providers sign with your domain as the d= value, which makes it align. That's why the modern setup requires both — SPF alone frequently doesn't align when you're using a third-party sender.

Alignment modeMatch rule
Relaxed (r)Subdomains count — mail.example.com aligns with example.com
Strict (s)Exact match required

Relaxed is the default and correct for most setups.

The things that still bite you

SPF, DKIM, and DMARC are the authentication layer. They're necessary but not sufficient. Three other things cause delivery problems even when the records are perfect:

  • Reputation. A new IP with no sending history looks suspicious regardless of authentication. Warm it up slowly — no more than a few hundred emails a day at first.
  • Content. Words like "free," "guaranteed," "act now," excessive exclamation marks, and image-heavy emails with little text all trigger spam filters. Authentication doesn't override content scoring.
  • Engagement. Gmail weights engagement heavily. If your recipients mark your mail as spam or delete without opening, your future mail goes to spam regardless of the DNS records. The only fix is to send mail people want.

The three records take an afternoon to set up. Reputation takes months. If you're starting a new sending domain, set up the records early and start sending small volumes to engaged recipients before you need to send anything critical.

Tools I actually used

ToolPurpose
MXToolboxSPF, DKIM, and DMARC lookups
mail-tester.comSend a test email, get a score out of 10
DMARC AnalyzerReport parsing without hosting
digVerify the records are actually published
dig TXT example.com +short
dig TXT selector1._domainkey.example.com +short
dig TXT _dmarc.example.com +short

If those three commands don't return the records you configured, nothing else matters. Check DNS first, then everything else.