Free tool

SPF Checker

Follow every include in a domain's SPF record, count DNS lookups against the limit of 10, and see exactly what breaks it.

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 -all

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

TermWhat it doesDNS lookup
ip4:192.0.2.0/24Matches the connecting IPv4 address or range.No
ip6:2001:db8::/32Matches the connecting IPv6 address or range.No
include:domainChecks another domain's SPF record and matches if that record passes.Yes, plus every lookup inside it
a, a:hostMatches the addresses in the A or AAAA records of your domain or the named host.Yes
mx, mx:domainMatches the addresses of the domain's mail servers.Yes
exists:domainMatches if the name, usually built from macros, has an A record.Yes
ptrMatches on reverse DNS. RFC 7208 says it should not be published.Yes
redirect=domainHands the whole check to another domain's record when nothing else matched.Yes
exp=domainNames a TXT record with an explanation for failures.No, only fetched later
allMatches 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 include and redirect we followed, nested under the record that referenced it. Each line shows how many IP ranges that record lists and how it ends, for example ends ~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: or include: 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 ip4 address that isn't valid (octets may not have leading zeros), a CIDR length out of range, a missing domain after include: 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 one exp= at most.
  • Problem: "{domain} has 12 MX hosts; an 'mx' mechanism may resolve at most 10." Replace mx with the ip4 and ip6 ranges of the servers that actually send.
  • Problem: "'+all' authorizes every server on the internet to send as your domain." End the record with ~all or -all instead. 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 ~all or -all as the last term, or a redirect= 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, ip6 or an include instead.
  • Note: "redirect= is ignored because the record already ends with 'all'." RFC 7208 ignores redirect= whenever all is 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, ptr and exists costs one lookup, and so does redirect=, which we only follow when the record has no all. Terms inside included records count toward the same total. ip4, ip6, all and exp= 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 a and exists we look up the name's addresses, and for mx its 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. An mx term that finds more than 10 mail servers is an error on its own.
  • ptr is 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.

Related tools and guides

Frequently asked questions

+What is the SPF 10 lookup limit?

Receivers stop evaluating SPF after 10 DNS lookups caused by include, a, mx, ptr, exists and redirect. Lookups inside included records count too. Going over the limit makes SPF fail with a permanent error for every message whose check reaches it, even from servers you listed.

+Why is my lookup count higher than my number of includes?

Because the count follows every include into the provider's own record. One include that points to a record with three more includes costs four lookups. The include tree in the result shows where each lookup comes from.

+What is an empty lookup?

A lookup that returns no records, or a name that doesn't exist. RFC 7208 asks receivers to allow at most 2, so a third one is a permanent error and SPF can't pass. They usually come from a: or mx: terms for retired hosts and includes for services that removed their record.

+Can I have two SPF records?

No. A domain must publish exactly one TXT record starting with v=spf1. With two, receivers return a permanent error and SPF can't pass. Merge the mechanisms into a single record.

+Should my SPF record end with -all or ~all?

Both are fine once DMARC is in place, because DMARC decides the final action. Even the big providers differ: Google recommends ~all for Google Workspace, while Microsoft recommends -all for Microsoft 365. Never use +all, which authorizes every server on the internet.

+Why does SPF pass but DMARC still fail?

Many email services send with their own return-path domain, so SPF passes for their domain, not yours. DMARC needs alignment with your From domain. For those services, DMARC passes through DKIM instead.

+Does the checker test whether a particular server may send?

No. It checks the record itself: every term, every include and the lookup count a receiver needs in the worst case. To see how one real message did, paste its headers into the email header analyzer.

We use Google Analytics cookies only if you accept. Privacy policy