By Ahmet Karaca · Updated · Checked against RFC 7208 (SPF), Google Workspace's SPF setup guide and Microsoft's SPF guide for Microsoft 365 on
How SPF works
SPF is a single TXT record that lists the servers allowed to send mail for your domain. Receivers check the server that delivered a message against that list. Each include: pulls in another company's list, which is how Google Workspace, Microsoft 365 or your newsletter tool get authorized. For the full picture, from the envelope sender to what DMARC does with the result, read What is SPF?
How an SPF record is written
SPF lives in one TXT record on the domain that appears in the envelope sender of your mail, the address receivers record as the Return-Path. A receiver takes the IP address that connected to it and walks your record from left to right. The first term that matches decides the result, and nothing after it is evaluated. Here is a typical record:
v=spf1 ip4:192.0.2.10 include:_spf.mailhost.example mx -allIt must begin with exactly v=spf1; RFC 7208 discards a record that starts with anything else, even v=spf10. Each term after it is a mechanism or a modifier, and a mechanism can carry a qualifier: + pass (the default when none is written), - fail, ~ softfail and ? neutral. If no term matches and there is no redirect=, the result is neutral, as if the record ended in ?all.
| Term | What it does | DNS lookup |
|---|---|---|
| ip4:192.0.2.0/24 | Matches the connecting IPv4 address or range. | No |
| ip6:2001:db8::/32 | Matches the connecting IPv6 address or range. | No |
| include:domain | Checks another domain's SPF record and matches if that record passes. | Yes, plus every lookup inside it |
| a, a:host | Matches the addresses in the A or AAAA records of your domain or the named host. | Yes |
| mx, mx:domain | Matches the addresses of the domain's mail servers. | Yes |
| exists:domain | Matches if the name, usually built from macros, has an A record. | Yes |
| ptr | Matches on reverse DNS. RFC 7208 says it should not be published. | Yes |
| redirect=domain | Hands the whole check to another domain's record when nothing else matched. | Yes |
| exp=domain | Names a TXT record with an explanation for failures. | No, only fetched later |
| all | Matches everything, so it goes last and decides what happens to unlisted servers. | No |
redirect= and exp= may each appear once; a second copy makes the check fail with a permanent error. Unknown modifiers are ignored. A TXT value longer than 255 characters has to be split into several quoted strings inside the same record, and receivers join them without adding spaces, so put the break inside a term or keep the space at the end of a string. RFC 7208 also asks you to keep the whole DNS answer small: under about 450 bytes of name and record text, so that it fits in a single UDP packet.
What each part of the result means
- DNS lookups. The number of terms that cost a lookup, counted across your record and every record it pulls in, next to the limit of 10. Over the limit, receivers return a permanent error and SPF can't pass.
- Empty lookups. Lookups that came back with no records or with "this name doesn't exist". RFC 7208 lets receivers fail the check after 2 of them, so a third one is already a problem.
- The record. The TXT record exactly as your domain publishes it, so you can compare it with what you meant to publish.
- Include tree. Every
includeandredirectwe followed, nested under the record that referenced it. Each line shows how many IP ranges that record lists and how it ends, for exampleends ~all. A domain marked broken carries an error of its own, such as a missing record or a syntax mistake. - Findings. Problems first, then warnings, then notes. Problems are things that make SPF fail or let anyone send as you; warnings weaken the record; notes explain something we couldn't check or a term receivers ignore. When nothing is wrong you see "No problems found".
Common errors and how to fix them
These are the findings the checker can report, with the message you see in the result. Names and numbers in the messages below are examples; your result shows the real ones.
- Problem: "{domain} has no SPF record, so receivers can't tell which servers may send for it." Publish one TXT record starting with
v=spf1. The SPF generator builds it from the services you use. - Problem: "{domain} has 2 SPF records. Only one is allowed; merge them into a single record." Receivers return a permanent error when they find more than one. Move every mechanism into one record and delete the other; merging multiple SPF records shows how.
- Problem: "Your SPF record needs 12 DNS lookups. Receivers stop at 10 and return a permanent error, so SPF can't pass." Remove includes for services you no longer use, or move a sender to its own subdomain. Fixing "too many DNS lookups" lists the options, safest first.
- Problem: "3 lookups in your SPF record return nothing. Past 2, receivers return a permanent error, so SPF can't pass." Look for
a:,mx:orinclude:terms that name hosts or services you retired, and delete them. - Problem: "{domain} is referenced but has no SPF record, which makes your whole SPF check fail." RFC 7208 turns an include or redirect that finds no record into a permanent error. Check the name for typos against your provider's documentation, or remove the term.
- Problem: "{domain} includes itself through a → b → a, which creates a loop receivers will fail on." Break the chain by removing one of the includes; the message names every domain in the loop.
- Problem: "'inlcude:_spf.google.com' is not a valid SPF mechanism." Fix the spelling or the value. The same code covers an
ip4address that isn't valid (octets may not have leading zeros), a CIDR length out of range, a missing domain afterinclude:and a name that isn't a valid domain. - Problem: "The redirect= modifier appears more than once, so receivers return a permanent error and SPF can't pass." Keep one
redirect=and oneexp=at most. - Problem: "{domain} has 12 MX hosts; an 'mx' mechanism may resolve at most 10." Replace
mxwith theip4andip6ranges of the servers that actually send. - Problem: "'+all' authorizes every server on the internet to send as your domain." End the record with
~allor-allinstead. Softfail vs fail explains the choice. - Warning: "'?all' offers no protection. Use '~all' or '-all'." A neutral result tells receivers nothing about unlisted servers.
- Warning: "There is no 'all' at the end, so servers you didn't list get a neutral result instead of a fail." Add
~allor-allas the last term, or aredirect=if another record should decide. - Warning: "Anything after 'all' is never evaluated." Move those terms in front of
all, or delete them if the service is gone. - Warning: "The ptr mechanism is slow and RFC 7208 §5.5 says not to publish it. Replace it with ip4, ip6 or include." RFC 7208 also calls it unreliable when DNS has errors. The servers it was meant to cover can be listed with
ip4,ip6or an include instead. - Note: "redirect= is ignored because the record already ends with 'all'." RFC 7208 ignores
redirect=wheneverallis present. Remove one of them. - Note: "'%{i}._spf.example.com' uses macros, which are expanded per message and can't be checked here." Nothing to fix. The lookup still counts toward the 10, but what it returns depends on each message.
- Note: "We stopped following includes after 30 lookups." The record is far over the limit; the lookup count is then a minimum, shown as "at least".
- Problem: "We couldn't query DNS for {domain}. Try again in a minute." A DNS server didn't answer in time. We don't keep this result, so checking again asks DNS afresh.
How we check
We read the TXT records of the domain you enter and keep the one that starts with v=spf1. Then we follow the record the way a receiver would, term by term, up to all:
- Each
include,a,mx,ptrandexistscosts one lookup, and so doesredirect=, which we only follow when the record has noall. Terms inside included records count toward the same total.ip4,ip6,allandexp=cost nothing. - When two includes lead to the same domain, we fetch its record once but count the lookup each time, because a receiver evaluating your record would do the same work twice.
- For
aandexistswe look up the name's addresses, and formxits mail servers. An answer with no records, or a name that doesn't exist, counts as an empty lookup. So does an include or redirect whose domain has no TXT records at all. Anmxterm that finds more than 10 mail servers is an error on its own. ptris counted but not resolved, because its answer depends on the IP of each connecting server. Terms with macros such as%{i}are counted too, but they can only be expanded for a real message, so we say we skipped them.- A loop, where a domain ends up including itself, is reported with the chain of domains that forms it. To protect DNS from broken or hostile chains we stop following includes after 30 lookups; by then the record is long past the limit of 10.
The count is the worst case: the number of lookups a receiver needs for a server that matches none of your terms, such as a forger. A real message from a server listed early in the record can pass before the receiver reaches the later terms, which is why a record over the limit often fails only some of your mail. We check the record itself, not one particular sending IP, so the result says whether the record holds up for every message rather than whether one message passed.
The limits we apply are the ones in RFC 7208: 10 lookups and 2 empty lookups per check, and 10 mail servers per mx term. A check has 10 seconds to finish. We reuse a finished result for 5 minutes, unless a DNS query failed, so if you have just changed the record and still see the old one, wait a few minutes; your DNS host's TTL can delay the change too.
Build a clean record with the SPF generator, then check it here again after publishing.