SPF, DKIM and DMARC explained: how to read the records and the mistakes that get mail rejected
Published 15 Sep 2026 · Updated 16 Sep 2026
Three records, one job each
Mail has no built-in way to prove who sent a message. The From: line is a string anyone can type. Three DNS records give a receiving server three different checks, and its decision to deliver, junk or reject rests on what they say.
SPF (RFC 7208) lists the servers allowed to send mail for your domain. The receiver looks up the record, checks the connecting server's address against it, and gets a pass, a fail or something in between.
DKIM (RFC 6376) signs each message. Your sending server adds a signature header computed with a private key; the public key is published in DNS under a selector; the receiver fetches it and verifies that the signed headers and the body arrived unchanged.
DMARC (RFC 7489) is the policy on top: what a receiver should do with a message from your domain that passes neither check, and where to send reports about what it saw. Without DMARC, SPF and DKIM are advice. With it, they are rules.
hostnag reads all three for a mail domain on every poll, along with the MX records and up to 8 sending addresses on 4 public blocklists, and the mail check reads them once for anyone.
Reading an SPF record
An SPF record is one TXT record at the domain itself, starting v=spf1, followed by mechanisms read left to right until one matches:
v=spf1 ip4:203.0.113.0/24 include:_spf.google.com -all
Each term is a mechanism with an optional qualifier in front. ip4: and ip6: name addresses or ranges directly. a and mx mean the domain's own address records or mail exchangers. include: pulls in another domain's SPF record, which is how a provider (Google, Microsoft, a transactional mail service) is authorised without copying its addresses. all matches everything and goes last, and its qualifier is the verdict for a server nothing else matched: -all fails it, ~all soft-fails it (a hint to the receiver rather than an order), ?all says nothing, and +all, or a bare all, passes it.
The rule that bites is the lookup limit. RFC 7208 allows at most ten DNS lookups while evaluating a record: every include:, a, mx, exists: and ptr mechanism costs one, so does a redirect=, and an included record's own lookups count too. ip4:, ip6: and all cost nothing. Past ten, the receiver returns a permanent error, which counts as a failure for DMARC, and it does so for every message, because the record is broken rather than the sender. Records grow past the limit one provider at a time: a CRM, a helpdesk, a newsletter service, each adding an include: that brings its own includes with it.
Reading a DKIM record
A DKIM record lives at <selector>._domainkey.<domain>. The selector is a name the sending system chose (google, selector1, k1, a date), and it travels in the message as the s= tag of the DKIM-Signature header, next to d=, the signing domain. That is how a receiver knows where to fetch the key, and why one domain can hold many keys: one per sender, or one per rotation.
The record reads v=DKIM1; k=rsa; p=MIIBIjANBg.... The p= tag is the public key. A record whose p= is empty means the key has been revoked; the standard reserves that spelling for it, and messages signed with that selector fail from then on. Providers often ask you to publish the selector as a CNAME to a record they manage, so they can rotate the key without a change on your side.
Reading a DMARC record
The DMARC record is a TXT record at _dmarc.<domain>, starting v=DMARC1:
v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; pct=100; adkim=r; aspf=r
p= is the policy for a message that fails: none (deliver, only report), quarantine (the junk folder) or reject (refuse). rua= is where receivers send aggregate reports, XML summaries of every message they saw claiming your domain, usually daily; it is how you find out what is sending as you. pct= applies the policy to a share of failing mail, for rolling out. sp= sets a separate policy for subdomains. adkim= and aspf= set alignment, relaxed (r) or strict (s): DMARC asks not only whether SPF or DKIM passed but whether the domain that passed is the one in the From: line, exactly (strict) or as a parent (relaxed). A forwarded message often passes SPF for the forwarder's domain and not for yours, which is why DKIM, which usually survives forwarding, is what keeps DMARC from rejecting legitimate mail.
Look them up by hand
dig answers each in a line. Replace the selector with the one from a real message's DKIM-Signature header.
dig +short txt example.com | grep spf1
dig +short txt _dmarc.example.com
dig +short txt selector1._domainkey.example.com
The first command filters the apex TXT records, because SPF shares that name with the verification tokens of every service that ever asked you to prove you owned the domain. If it prints two lines, that is the first mistake below.
The mistakes that get mail rejected
Two SPF records. The standard says a domain with more than one v=spf1 record produces a permanent error, the same as a broken record. It happens when a second provider's instructions say "add this TXT record" and someone does, instead of adding an include: to the record that is already there. Merge them into one record with one all at the end. hostnag reports two records as a finding, with a warning, on its next poll.
+all. Every server on the internet passes SPF for the domain. It usually arrives as a quick fix for a bounce nobody could explain, and it stays because mail keeps working. It is the record saying that the domain has no sender policy at all.
A DKIM selector rotated away. A provider retires a selector, or someone cleans up "unused" DNS records, and the key a signing server still uses is not at the name the receiver looks up. Every message from that source now fails DKIM; with SPF still passing, DMARC may hold, but a forwarded copy fails both. hostnag looks up every selector you name, up to 10 per domain, on each poll: a selector with no record and a record with an empty key are separate findings, and a key that changes under a selector is a Changed alert with the old and the new record in it.
DMARC left at p=none. none is the right first setting: it turns the reports on without changing delivery, so you can find every legitimate sender before enforcing anything. It is the wrong permanent setting, because it tells receivers to deliver mail that failed both checks. Read the reports, add the missing senders to SPF and DKIM, move to p=quarantine with a low pct=, then to p=reject. hostnag shows a domain sitting at p=none as an informational finding: it pages nobody, and it is not forgotten either.
And the quiet one: more than ten lookups. The mail check counts the lookups your own record makes (what an include: brings along is counted at the receiver as well), and the monitor raises a warning when the count is over the limit.
What a monitor adds to a one-time check
Every one of these records is read from DNS by the receiver at the moment a message arrives, so a change to DNS changes your deliverability the same minute, with nothing on your side to see. hostnag re-reads MX, SPF, DMARC and every named DKIM selector at least once a day on every plan (1440 minutes on Free), and as often as every 60 minutes on Pro and every 15 on Business. A record that goes missing or breaks is a finding with a warning; a record that changes from one value to another is a Changed alert carrying both values, whether the edit was planned or not. A lookup that did not answer is left out of the report rather than counted as clean, so a resolver hiccup never reads as a missing record.
Check it now, then watch it
Check it now: paste a domain and its DKIM selectors into the mail check to read MX, SPF, DKIM and DMARC and count the SPF lookups, or pull a single record with the DNS lookup.
Watch it for good: mail health monitoring reads the records on its schedule, flags what is missing, doubled or over the limit, and tells you when one of them changes.
Check it now
Watch it for good
Watch this for good.
Start free