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:
| Record | What it proves | Where it lives |
|---|---|---|
| SPF | Which servers are allowed to send for your domain | TXT record |
| DKIM | That the message hasn't been modified in transit | TXT record + signature header |
| DMARC | What to do when SPF and DKIM fail, and where to send reports | TXT 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:
- Receiving server looks up the SPF record for the domain in the
MAIL FROMheader (the envelope sender, not the visible From address). - It compares the connecting IP against the list.
- It returns a result:
pass,fail,softfail, orneutral.
The qualifier on each mechanism tells the receiver how to treat a match or mismatch:
| Qualifier | Meaning | Typical use |
|---|---|---|
+ | Pass | Default, 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
| Mistake | Result |
|---|---|
| Wrong selector name | DNS lookup fails, DKIM fails silently |
| Public key truncated by DNS | Signature verification fails |
Missing v=DKIM1 in the record | Some receivers reject the record |
| Mailing list modifying subject lines | DKIM 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"
| Tag | Meaning |
|---|---|
p=none | Take no action, just report — good for the first two weeks |
p=quarantine | Send failures to spam |
p=reject | Reject failures outright — the goal |
rua | Aggregate report address (daily summaries) |
ruf | Forensic report address (per-message failures; many providers ignore this) |
pct | Percentage of messages to which the policy applies |
adkim, aspf | r for relaxed (allow subdomain match), s for strict |
The rollout sequence
- Start with
p=noneand aruaaddress. Wait two weeks. Reports arrive daily, listing every source sending mail on your behalf. - 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.
- Move to
p=quarantine; pct=25. Watch reports for a week. If your legitimate mail still passes, increase topct=50, thenpct=100. - 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
ruatag. You'll getp=nonewith 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 mode | Match 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
| Tool | Purpose |
|---|---|
| MXToolbox | SPF, DKIM, and DMARC lookups |
| mail-tester.com | Send a test email, get a score out of 10 |
| DMARC Analyzer | Report parsing without hosting |
dig | Verify 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.
