# SPF, DKIM, and DMARC explained for developers

> Why email from your domain lands in spam, what SPF, DKIM and DMARC each verify, example DNS records dissected, and the rollout order.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-08 | Updated: 2026-08-06 | Topics: [Networking](https://flaviocopes.com/tags/network/) | Canonical: https://flaviocopes.com/spf-dkim-dmarc-explained/

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 `billing@yourbank.com`. 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.

```mermaid
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 this
- `include:_spf.google.com` — "also trust every IP in Google's SPF record". This is how you authorize Google Workspace without listing its IPs yourself
- `include:amazonses.com` — same, for Amazon SES (which is what Resend uses under the hood)
- `~all` — the qualifier for everything else. `~all` is **soft fail**: mail from other IPs is suspicious but may still be delivered. `-all` is **hard fail**: reject it. Start with `~all`, move to `-all` when 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 uses `google`, SendGrid uses `s1` and `s2`, Postmark uses `pm`). Selectors let multiple providers sign for the same domain without colliding
- `v=DKIM1` — version
- `k=rsa` — key type
- `p=...` — 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](https://flaviocopes.com/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:dmarc@flaviocopes.com; pct=100; adkim=r; aspf=r"
```

- `v=DMARC1` — version
- `p=quarantine` — the policy for failing mail. `none` = deliver but report, `quarantine` = send to spam, `reject` = refuse it
- `rua=mailto:...` — where receivers send aggregate reports: XML summaries of who sent mail claiming to be you, and whether it passed
- `pct=100` — apply the policy to this percentage of failing mail. Lets you ramp up gradually
- `adkim=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:

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:

```bash
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](https://flaviocopes.com/dns/).)

To build the records instead of hand-writing them, I made an [email DNS tool](https://flaviocopes.com/tools/email-dns/) — 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](https://flaviocopes.com/resend-transactional-email-workers/).

Three TXT records. That's the difference between "why is my email in spam" and reliable delivery.
