Skip to content
FLAVIO COPES
flaviocopes.com

SPF, DKIM, and DMARC explained for developers

By

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:

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..."

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"

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:

  1. Set up SPF and DKIM first, and verify both pass
  2. Publish DMARC with p=none and a rua address. Nothing changes for delivery — you just get reports. Watch them for 2–4 weeks and find every legitimate sender you forgot
  3. Move to p=quarantine, starting with pct=25, then 50, then 100
  4. 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: