How to check SPF record
Query the domain's TXT records and keep the one that starts with v=spf1: nslookup -type=TXT github.com 8.8.8.8. A domain publishes exactly one such record. Read the all mechanism at its end, because that qualifier is what a receiver applies to every sender the record did not list.
Why check this
Run this when the sending path changes: a new transactional provider, a new marketing tool, a move of the mail platform, or a DNS cutover. The failure it prevents is specific. Password reset mail starts leaving through a provider whose addresses are not in the record, the receiver applies the all qualifier instead, and the message is filed as spam or rejected while the application logs a successful send.
The check is a read, not a test send. It tells you which senders the domain claims, whether the record parses, and what happens to everything else. It does not tell you whether your application actually sends from one of those addresses, and it says nothing about the From: header your users see. Those belong to How to check DMARC alignment.
Prerequisites
- A resolver you name in the command. Answers differ between resolvers during a change, so
8.8.8.8in the command beats whatever the laptop inherited from the hotel wifi. - RFC 7208 for mechanism and qualifier meanings.
- Node 22 for step 3. Save this as
spf-record.js:
const { Resolver } = require('dns');
const r = new Resolver();
r.setServers(['8.8.8.8']); // name the resolver: Node's default is not always usable
r.resolveTxt(process.argv[2], (err, recs) => {
if (err) return console.log('lookup failed: ' + err.code);
const spf = recs.filter(chunks => chunks.join('').startsWith('v=spf1'));
console.log('SPF records : ' + spf.length + (spf.length === 1 ? '' : ' <-- must be exactly 1'));
if (spf.length !== 1) return;
const rec = spf[0].join(''); // RFC 7208 section 3.3: concatenate, no separator
console.log('TXT strings : ' + spf[0].length + ' (lengths ' + spf[0].map(s => s.length).join(', ') + ')');
console.log('joined length : ' + rec.length);
const terms = rec.split(/\s+/).slice(1);
const all = terms.find(t => /^[-~?+]?all$/i.test(t));
console.log('all mechanism : ' + (all || 'MISSING, evaluation ends in neutral'));
console.log('ptr mechanism : ' + (terms.some(t => /^[-~?+]?ptr/i.test(t)) ? 'present, RFC 7208 section 5.5 says do not publish it' : 'none'));
console.log('macros : ' + (terms.filter(t => t.includes('%{')).join(' ') || 'none'));
console.log('record : ' + rec);
});
Steps
- Step 1.
Ask a named resolver for the TXT records of the domain.
nslookup -type=TXT github.com 8.8.8.8Non-authoritative answer: Server: dns.google Address: 8.8.8.8 github.com text = "krisp-domain-verification=ZlyiK7XLhnaoUQb2hpak1PLY7dFkl1WE" github.com text = "facebook-domain-verification=39xu4jzl7roi7x0n93ldkxjiaarx50" github.com text = "google-site-verification=UTM-3akMgubp6tQtgEuAkYNYLyYAvpTnnSrDMWoDR3o" …24 TXT records in totalSPF lives among the vendor verification strings. Nothing marks it apart except the
v=spf1prefix. - Step 2.
Filter the listing down to the SPF record.
nslookup -type=TXT github.com 8.8.8.8 | grep -i spf1"v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com include:_netblocks.google.com include:_netblocks2.google.com include:mail.zendesk.com include:_spf.salesforce.com include:servers.mcsv.net include:mktomail.com include:sendgrid.net ip4:62.253.2"Read the end of that line. It stops at
ip4:62.253.2, which is not an address, and there is noallmechanism. The record is not broken; the filter cut it. Step 3 shows why. - Step 3.
Read the record through a resolver that joins the TXT strings.
node spf-record.js github.comSPF records : 1 TXT strings : 2 (lengths 255, 65) joined length : 320 all mechanism : ~all ptr mechanism : none macros : none record : v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com include:_netblocks.google.com include:_netblocks2.google.com include:mail.zendesk.com include:_spf.salesforce.com include:servers.mcsv.net include:mktomail.com include:sendgrid.net ip4:62.253.227.114 ip4:166.78.69.169 ip4:166.78.69.170 ip4:166.78.71.131 ~allThe record is 320 characters, so DNS carries it as two strings of 255 and 65. The split falls inside an address:
ip4:62.253.2plus27.114isip4:62.253.227.114. - Step 4.
Compare with a domain that sends no mail at all.
node spf-record.js example.comSPF records : 1 TXT strings : 1 (lengths 11) joined length : 11 all mechanism : -all ptr mechanism : none macros : none record : v=spf1 -all - Step 5.
Check whether the record depends on macros, which no static read can resolve.
node spf-record.js mozilla.orgSPF records : 1 TXT strings : 1 (lengths 137) joined length : 137 all mechanism : ~all ptr mechanism : none macros : include:%{i}._ip.%{h}._ehlo.%{d}._spf.vali.email record : v=spf1 include:%{i}._ip.%{h}._ehlo.%{d}._spf.vali.email include:_spf.mozilla.com include:_spf.google.com include:spf.fundraiseup.com ~all%{i}is the connecting IP address and%{h}is the HELO name. The include target only exists at evaluation time, so the answer for one sender says nothing about another.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| -all at the end | Everything not listed fails SPF | Confirm every sending system is listed before publishing this. |
| ~all at the end | Everything not listed is a soft fail | Normal while a sending path is still being inventoried. |
| ?all or +all | The record authorises nobody, or everybody | +all authorises the whole internet. Remove it. |
| No all mechanism | Evaluation ends in neutral | Add one. A record without it decides nothing. |
| SPF records : 2 | Every check returns permerror | Merge the two records into one. Both are ignored until you do. |
| A %{ macro in a term | The target is built from the sender | Static reads cannot verify it. Test with a real send instead. |
Common mistakes
What to check next
- How to check SPF lookup limit: the record above sits on the ten-lookup ceiling, which the text does not show.
- How to check DMARC record: the policy that decides what a receiver does with an SPF result.
- How to check DKIM record: the second authentication method, and the one that survives forwarding.
- How to check DMARC alignment: why an SPF pass can still leave DMARC failing.
- Email testing checklist: the full set of reads before a release that touches mail.
FAQ
How to check an SPF record for a domain?
Query TXT on the domain itself, never on a _spf prefix. _spf.example.com is a common include target, not the record a receiver looks up. The record that counts is the one on the bare domain in the envelope sender.
How to check an SPF record using nslookup?
nslookup -type=TXT example.com 8.8.8.8. Read the whole answer rather than a grepped line, because a record over 255 characters arrives as several quoted strings on separate lines and they belong together.
How to test if an SPF record is working?
A read tells you the record parses. Only a real message tells you it passes, because the result depends on the sending IP and the envelope domain. Send one to a mailbox you control and read its Authentication-Results header.
Does an SPF record stop spoofing?
On its own, no. SPF covers the envelope sender, which the recipient never sees. Stopping a forged From: header needs a DMARC policy, which is checked in How to check DMARC record.
Verified
Verified by Maks Vernynslookup Windows 10.0.22631node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic4 minpublished updated Maks Verny