SPF, DKIM, and DMARC explained for developers
By Flavio Copes
Why email from your domain lands in spam, what SPF, DKIM and DMARC each verify, example DNS records dissected, and the rollout order.
You wire up transactional email, send a test, and it lands in spam. Or worse: it delivers fine for weeks, then Gmail suddenly starts rejecting everything.
The cause is almost always the same. Your domain is missing one or more of three DNS records: SPF, DKIM, and DMARC.
Here’s the underlying problem they solve: SMTP lets anyone put anything in the From field. Nothing in the protocol stops me from sending email as [email protected]. So receiving servers stopped trusting the From field and started checking DNS instead.
Gmail requires every sender to use SPF or DKIM. Bulk senders, around 5,000 messages per day to Gmail accounts, need SPF, DKIM, and DMARC. Yahoo has similar bulk-sender requirements.
Each record verifies a different thing. Let’s take them one at a time.
flowchart TD
accTitle: SPF DKIM and DMARC checks
accDescr: A receiving mail server checks the sending IP with SPF, verifies the message signature with DKIM, then uses DMARC to check alignment with the visible From domain and apply policy.
Mail["Incoming email"] --> SPF["SPF<br/>Is this sending IP allowed?"]
Mail --> DKIM["DKIM<br/>Is the signature valid?"]
SPF --> DMARC["DMARC<br/>Does SPF or DKIM pass and align with From?"]
DKIM --> DMARC
DMARC --> Policy["Deliver, quarantine, or reject"]
SPF: which servers may send for your domain
SPF (Sender Policy Framework) is a TXT record on your domain that lists the IP addresses allowed to send email for it.
When mail arrives, the receiving server looks at the envelope sender (the return-path address used in the SMTP conversation, not the From header you see) and checks: did this message come from an IP the domain’s SPF record allows?
A typical record:
flaviocopes.com. TXT "v=spf1 include:_spf.google.com include:amazonses.com ~all"
Field by field:
v=spf1— the version tag. Every SPF record starts with thisinclude:_spf.google.com— “also trust every IP in Google’s SPF record”. This is how you authorize Google Workspace without listing its IPs yourselfinclude:amazonses.com— same, for Amazon SES (which is what Resend uses under the hood)~all— the qualifier for everything else.~allis soft fail: mail from other IPs is suspicious but may still be delivered.-allis hard fail: reject it. Start with~all, move to-allwhen you’re sure the record is complete
Two rules that bite people:
One record only. Two TXT records both starting with v=spf1 is an error, and SPF fails entirely. If you add a second provider, merge it into the existing record.
Max 10 DNS lookups. Every include: costs at least one lookup, and includes nest. Stack enough providers and you silently break SPF with a permerror.
DKIM: proving the message wasn’t forged
SPF checks the sending IP. DKIM (DomainKeys Identified Mail) checks the message itself, with cryptography.
Your email provider signs each outgoing message with a private key. The signature covers the body and key headers, and travels in a DKIM-Signature header. The receiving server fetches your public key from DNS and verifies the signature.
If it verifies, two things are proven: the message really was authorized by the domain, and nobody modified it in transit.
The public key lives at <selector>._domainkey.<yourdomain>:
resend._domainkey.flaviocopes.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3..."
resend— the selector. Providers choose their own (Google usesgoogle, SendGrid usess1ands2, Postmark usespm). Selectors let multiple providers sign for the same domain without collidingv=DKIM1— versionk=rsa— key typep=...— the base64-encoded public key
You never generate this by hand. Your provider gives you the exact record in its dashboard; you paste it into DNS. If you’re sending with Node, I covered the app side in how to send an email using nodemailer — DKIM is the DNS half of the same picture.
DMARC: alignment, policy, and reports
Here’s the gap the first two leave open. SPF checks the envelope sender. DKIM checks the signing domain. Neither checks the From header the user actually sees. A spammer can pass both — using their own domain — while displaying yours in From.
DMARC closes it with alignment: SPF or DKIM must pass and the domain they validated must match the From header domain. Then it adds a policy telling receivers what to do when that fails.
The record lives at _dmarc.<yourdomain>:
_dmarc.flaviocopes.com. TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100; adkim=r; aspf=r"
v=DMARC1— versionp=quarantine— the policy for failing mail.none= deliver but report,quarantine= send to spam,reject= refuse itrua=mailto:...— where receivers send aggregate reports: XML summaries of who sent mail claiming to be you, and whether it passedpct=100— apply the policy to this percentage of failing mail. Lets you ramp up graduallyadkim=r/aspf=r— relaxed alignment: subdomains of the From domain count as aligned.s(strict) requires an exact match
The rollout order
Don’t jump straight to p=reject. You’ll block your own newsletter tool, your CRM, that one server sending cron alerts you forgot about. Do it in stages:
- Set up SPF and DKIM first, and verify both pass
- Publish DMARC with
p=noneand aruaaddress. Nothing changes for delivery — you just get reports. Watch them for 2–4 weeks and find every legitimate sender you forgot - Move to
p=quarantine, starting withpct=25, then 50, then 100 - When the reports are clean, switch to
p=reject
The raw reports are gzipped XML and unpleasant to read. Free tools like Postmark’s DMARC digest turn them into a weekly email.
Checking your setup
You can inspect all three records for any domain with dig:
dig TXT flaviocopes.com +short
dig TXT resend._domainkey.flaviocopes.com +short
dig TXT _dmarc.flaviocopes.com +short
(For a refresher on what’s happening behind these queries, see how DNS works.)
To build the records instead of hand-writing them, I made an email DNS tool — pick your provider (Google Workspace, Resend, SendGrid, Mailgun, Postmark, SES), and it generates the SPF record with a lookup counter, the DKIM record in the right format, and a DMARC record with the rollout knobs.
Once DNS is sorted, sending the actual mail is the easy part — for example from a Cloudflare Worker with Resend.
Three TXT records. That’s the difference between “why is my email in spam” and reliable delivery.
Related posts about network: