Email breaks in the most insulting way: it “mostly works” until the day it doesn’t, and then your execs learn the phrase SPF permerror from a bounce message at 6:12 a.m.
This is about the dumb failures—the quoting and whitespace gotchas in SPF TXT records that turn a perfectly valid policy into a silent fail, an intermittent fail, or a vendor-blame festival. If you run production systems, you don’t need hand-wavy “check your DNS.” You need a checklist, a fast diagnosis playbook, and a strong opinion about where humans should not be allowed to type double quotes.
Why formatting breaks SPF (even when the text looks right)
SPF is text. DNS is text. Humans love text. That’s the problem.
SPF is evaluated by receivers and gateways that parse a TXT record into a sequence of mechanisms and modifiers. The parsing rules are strict in the boring ways and surprisingly permissive in the confusing ways. The result: a record can look fine in a DNS UI, look fine in a screenshot, and still be interpreted differently across resolvers, libraries, and mail receivers.
Formatting errors fall into a few buckets:
- DNS presentation vs DNS wire format: tools like
digshow quotes; DNS providers store strings; libraries join “character-strings” together. People copy the quotes into the value and accidentally publish literal quotes. - Whitespace and tokenization: SPF mechanisms are separated by spaces. Extra spaces are usually tolerated, but not always where you think. Tabs, non-breaking spaces, and UI “smart formatting” can create tokens that don’t parse as expected.
- Splitting across strings: DNS TXT RDATA can contain multiple character-strings. Many providers split long values. SPF processing concatenates them without adding spaces. If the split happens at the wrong place, you can weld two tokens into one invalid token.
- Multiple SPF records: more than one SPF policy at the same name is a protocol error. This is not “it will pick one.” It’s “some receivers treat it as permerror and fail SPF.”
- Lookups and redirects: not formatting, but it shows up in the same incident queue. Includes/redirects explode the DNS lookup count, and SPF returns permerror. People often “fix” this by flattening, which introduces new formatting hazards.
One more reason this hurts: SPF failures don’t always bounce mail. Many receivers treat SPF as a signal, not a hard gate, especially when DMARC policy is relaxed. So you won’t see a clean outage—just “deliverability issues,” spam folder placement, and a slow leak of trust.
Fast diagnosis playbook (first/second/third)
When you’re on-call and an exec forwards “Why did this vendor say our email is spoofed?” you don’t need philosophy. You need a triage order that converges.
First: verify what’s actually published (not what your DNS UI shows)
- Query TXT at the exact domain used in the MAIL FROM / Return-Path (often a subdomain). Many orgs edit
example.combut the sender usesbounces.example.com. - Count SPF records: if there’s more than one
v=spf1TXT value, treat it as a fire until proven otherwise. - Look for quoting artifacts: leading or trailing quote characters inside the published string, and “escaped quotes” that came from copy/paste.
Second: evaluate how receivers will parse it
- Join split TXT strings and re-check token boundaries (especially around
include:,ip4:,redirect=, and qualifiers like~all). - Run an SPF evaluation against a known sender IP (or at least validate syntax). Don’t assume “looks right” means “parses right.”
- Check the DNS lookup count if you’re near the edge; one new include can push you over 10.
Third: confirm what failed in the wild
- Get a real header sample from a failed recipient showing
Authentication-Results. That’s the receiver telling you how it interpreted your SPF. - Compare between receivers (Gmail vs Microsoft vs a security gateway). If one says “permerror” and another says “none,” you probably have multi-record, split, or resolver issues.
- Check propagation and caching before you keep flipping records like a slot machine.
Paraphrased idea from Werner Vogels (Amazon CTO): “Everything fails, all the time.” SPF formatting failures are just the email-flavored version of that truth.
Facts and historical context (the weird bits that matter)
- SPF started life as “Sender Permitted From” in the early 2000s, created to fight rampant spoofing and spam; it later became “Sender Policy Framework.” Names matter less than DNS mechanics.
- SPF is published in DNS TXT records largely for backwards compatibility. There was once a dedicated SPF RR type, but TXT won the deployment war.
- DNS TXT records can contain multiple “character-strings” (chunks). Tools display them with quotes, which tricks humans into thinking quotes are part of the value.
- SPF has a hard limit of 10 DNS lookups during evaluation (for mechanisms like
include,a,mx,ptr,exists, andredirect). One enthusiastic marketing tool can doom you. - SPF checks the envelope sender domain (MAIL FROM / Return-Path), not the “From:” header users see. That’s why SPF can pass while phishing still works.
- DMARC depends on alignment between SPF (envelope domain) and the header From domain, or DKIM alignment. SPF can be perfect and still fail DMARC if domains don’t align.
- Some receivers treat multiple SPF records as permerror and effectively “fail” SPF. Others pick one or merge—either way, it’s undefined enough to be dangerous.
- Whitespace in DNS is not a thing; whitespace in SPF is. DNS stores bytes. SPF interprets bytes as tokens. Confusing those layers causes the exact mistakes this article is about.
- Old SPF mechanisms like PTR were widely used and are now discouraged because they’re slow and brittle. You still find them hiding in legacy records like a forgotten cron job.
How SPF is parsed: what receivers actually evaluate
Let’s nail down the mental model, because most SPF formatting bugs come from having the wrong one.
Two layers: DNS TXT encoding vs SPF text parsing
DNS layer: a TXT record is one or more strings. Providers may store it as one long string, or split it. On the wire, it’s a list of length-prefixed chunks. Resolvers return the chunks; clients typically concatenate them.
SPF layer: once concatenated, SPF is a single string starting with v=spf1, followed by mechanisms and modifiers separated by spaces. Each mechanism can have an optional qualifier: + (pass), - (fail), ~ (softfail), ? (neutral).
What the receiver sees (simplified)
Receiver gets a connection from an IP. It reads the envelope sender domain (sometimes HELO name is used for SPF “HELO identity” checks, but the big one is MAIL FROM). It queries TXT for that domain. It selects the SPF record (if exactly one). It evaluates mechanisms until one matches. If it can’t evaluate (syntax error, DNS errors, too many lookups), you get permerror or temperror.
Permerror is not “kinda failed”
permerror means the published policy is broken or violates limits. Many receivers treat permerror like fail or like “no signal,” depending on their policy. That inconsistency is why formatting errors are so painful: you won’t get one clean failure mode.
Quoting and spaces: failure modes you can reproduce
1) Copying quotes from dig into DNS
dig prints TXT strings quoted. People see:
cr0x@server:~$ dig +short TXT example.com
"v=spf1 ip4:203.0.113.10 include:_spf.vendor.example ~all"
Then they paste the entire thing—including the quotes—into a DNS UI that already treats the value as a raw string. Result: the published value becomes "v=spf1 ... ~all" (with literal quote characters). Some SPF parsers choke immediately; others ignore leading quotes and then stumble later. It’s a great way to create a problem that only happens at one mailbox provider.
What to do: publish the text without surrounding quotes unless your DNS UI specifically requires them (and most modern ones don’t). If the UI shows quotes in its preview, that’s usually just presentation.
2) “Smart quotes” and non-breaking spaces from ticket systems
SPF expects ASCII spaces and ASCII punctuation. Paste from a formatted email or a chat tool and you can end up with:
- non-breaking spaces (U+00A0) instead of spaces
- curly quotes (“ ”) instead of straight quotes (“)
- en dashes (–) instead of hyphens (-) in rare cases (usually in copied domain names in prose)
Receivers vary in how they treat these. Many will treat the whole record as invalid.
3) Splitting a record across multiple TXT strings at the wrong place
This is the most common “it looks fine” breaker. DNS allows:
cr0x@server:~$ dig +short TXT example.com
"v=spf1 ip4:203.0.113.10 include:_spf.vendor.example"
" ~all"
Those two strings are concatenated into:
v=spf1 ip4:203.0.113.10 include:_spf.vendor.example ~all
That’s fine because the second string begins with a space.
But if your provider splits like this:
cr0x@server:~$ dig +short TXT example.com
"v=spf1 ip4:203.0.113.10 include:_spf.vendor.ex"
"ample ~all"
That concatenates to include:_spf.vendor.example ~all, still fine.
And if it splits like this:
cr0x@server:~$ dig +short TXT example.com
"v=spf1 ip4:203.0.113.10 include:_spf.vendor.example"
"~all"
Now you get include:_spf.vendor.example~all which is a single token. That’s garbage. Some parsers treat it as an unknown mechanism; others treat it as a syntax error. Either way, you’re not enforcing what you think you’re enforcing.
Rule: if a record is split, ensure the boundary is on a token boundary and that spaces are preserved. If in doubt, explicitly start the next string with a space.
4) Multiple TXT records that each contain v=spf1
This is a classic. Someone adds a vendor and “just adds another SPF record” because the DNS UI allows multiple TXT entries at the same name.
SPF says: one record. If you publish two, many evaluators return permerror. Some will pick one, which is worse because it “works” until a resolver changes behavior.
5) Extra spaces and tabs: not always harmless
Multiple spaces between mechanisms are typically okay. Tabs, carriage returns, or pasted weird whitespace are not reliably okay. Also: a trailing space at the end of a chunk can combine badly when concatenated with the next chunk.
6) Escaping that looks like escaping
TXT records in zone files sometimes use backslashes to escape quotes. DNS provider UIs vary: some expect raw text; some expect zone-file style. If you paste \"v=spf1 ...\" you may literally publish backslashes, quotes, and sadness.
Joke #1: SPF is the only place where a single missing space can tank revenue and still count as “just a text record.”
Practical tasks: commands, outputs, and decisions (12+)
These are field tasks: run the command, interpret the output, make a decision. Do them in order when you’re debugging, or pick the ones that match your symptoms.
Task 1: Identify the SPF identity domain from a real message
Command: extract Return-Path and Authentication-Results from a stored message (example using a saved raw email).
cr0x@server:~$ grep -E '^(Return-Path|Authentication-Results):' -n sample.eml
12:Return-Path: <bounces@mail.example.com>
45:Authentication-Results: mx.google.com; spf=permerror (google.com: domain of bounces@mail.example.com has multiple SPF records) smtp.mailfrom=bounces@mail.example.com
What it means: SPF is evaluated against mail.example.com (or possibly bounces subdomain), not necessarily example.com.
Decision: query TXT for the exact domain in smtp.mailfrom=. Don’t “fix” the apex if the mail is using a subdomain.
Task 2: Query TXT records as the world sees them
cr0x@server:~$ dig TXT mail.example.com +noall +answer
mail.example.com. 300 IN TXT "v=spf1 include:_spf.vendor.example ~all"
What it means: there is one TXT record and it contains one string (in this response).
Decision: proceed to validate syntax and lookup count; if there are multiple answers or multiple v=spf1 strings, stop and fix that first.
Task 3: Detect multiple SPF policies at one name
cr0x@server:~$ dig +short TXT mail.example.com | grep -i 'v=spf1'
"v=spf1 include:_spf.vendor.example ~all"
"v=spf1 ip4:203.0.113.10 ~all"
What it means: two SPF records. This is a protocol error and likely yields permerror somewhere.
Decision: merge mechanisms into a single SPF record; delete the extra.
Task 4: Check for literal quotes inside the published string
cr0x@server:~$ dig +short TXT mail.example.com
"\"v=spf1 include:_spf.vendor.example ~all\""
What it means: the TXT string includes actual quote characters at the beginning and end (escaped for display). That often breaks parsing.
Decision: edit the DNS record to remove the inner quotes; publish v=spf1 ... as plain text.
Task 5: Spot multi-string TXT splitting and verify boundaries
cr0x@server:~$ dig TXT example.com +noall +answer
example.com. 300 IN TXT "v=spf1 ip4:203.0.113.10 include:_spf.vendor.example" "~all"
What it means: two character-strings. They will be concatenated without an inserted space, resulting in ...vendor.example~all.
Decision: fix the split so the second string begins with a space, or keep it as one string if your provider supports it.
Task 6: Compare results from multiple resolvers (caching/proxy issues)
cr0x@server:~$ dig @1.1.1.1 +short TXT mail.example.com
"v=spf1 include:_spf.vendor.example ~all"
cr0x@server:~$ dig @8.8.8.8 +short TXT mail.example.com
"v=spf1 include:_spf.vendor.example" "~all"
What it means: different resolvers are returning different TXT chunking (still potentially equivalent), or you’re seeing partial propagation or a multi-provider DNS misconfiguration.
Decision: if the chunking changes token boundaries (as above), treat as broken; otherwise, watch TTL/propagation and verify your authoritative servers are consistent.
Task 7: Ask the authoritative name servers directly
cr0x@server:~$ dig NS example.com +short
ns1.dns-provider.example.
ns2.dns-provider.example.
cr0x@server:~$ dig @ns1.dns-provider.example TXT mail.example.com +noall +answer
mail.example.com. 300 IN TXT "v=spf1 include:_spf.vendor.example ~all"
cr0x@server:~$ dig @ns2.dns-provider.example TXT mail.example.com +noall +answer
mail.example.com. 300 IN TXT "v=spf1 include:_spf.vendor.example" "~all"
What it means: your authoritative servers disagree. That’s not “propagation.” That’s inconsistency.
Decision: stop editing SPF and fix the DNS publication pipeline (zone push, provider sync, or split-brain). Mail receivers will hit either.
Task 8: Validate SPF syntax with a local parser (quick sanity)
Install a tool (example package name varies by distro; shown here as if already installed) and validate the record text you intend to publish.
cr0x@server:~$ spfquery --scope mfrom --id bounces@mail.example.com --ip 203.0.113.10
pass: domain of bounces@mail.example.com designates 203.0.113.10 as permitted sender
What it means: for this IP, the SPF policy returns pass. Syntax is at least parseable for this tool.
Decision: if this returns permerror, fix formatting or lookup limits before chasing anything else.
Task 9: Force a permerror check by evaluating lookup-heavy records
cr0x@server:~$ spfquery --scope mfrom --id bounces@mail.example.com --ip 198.51.100.25
permerror: too many DNS lookups
What it means: you exceeded the 10-lookup SPF limit. This often happens after “just one more include.”
Decision: reduce includes/redirects, remove a/mx if unnecessary, or redesign sending architecture (dedicated subdomains per vendor). Don’t paper over it with random flattening unless you control the update process.
Task 10: Inspect record text for non-ASCII whitespace
Pull the TXT record and look at bytes. This catches non-breaking spaces and other invisible sabotage.
cr0x@server:~$ dig +short TXT mail.example.com | tr -d '\n' | hexdump -C | head
00000000 22 76 3d 73 70 66 31 c2 a0 69 6e 63 6c 75 |"v=spf1..inclu|
00000010 64 65 3a 5f 73 70 66 2e 76 65 6e 64 6f 72 |de:_spf.vendor|
00000020 2e 65 78 61 6d 70 6c 65 20 7e 61 6c 6c 22 |.example ~all"|
What it means: c2 a0 is a non-breaking space (UTF-8). That is not a normal ASCII space.
Decision: retype the record in a plain-text editor or DNS UI without fancy whitespace. Treat anything non-ASCII as suspect.
Task 11: Confirm what your MTA is using as envelope sender
Postfix example: check smtpd_sender_restrictions doesn’t rewrite, and check canonical maps if used.
cr0x@server:~$ postconf -n | egrep 'myhostname|mydomain|sender_canonical|smtp_generic_maps'
myhostname = mx1.internal.example
mydomain = example.com
sender_canonical_maps =
smtp_generic_maps =
What it means: no canonical rewriting configured here (at least in this snippet). If you expected a subdomain bounce address, it may be set in your application or ESP, not Postfix.
Decision: identify the true MAIL FROM domain in actual sent messages (Task 1) and publish SPF there.
Task 12: Pull authoritative SPF record and normalize to one line
This makes it easier to spot welded tokens due to splitting.
cr0x@server:~$ dig @ns1.dns-provider.example TXT mail.example.com +short | tr -d '\n' | sed 's/" "/ /g'
"v=spf1 include:_spf.vendor.example ~all"
What it means: after naive normalization, you can see whether there’s a missing space between chunks.
Decision: if you see example~all or cominclude: patterns, fix chunk boundaries.
Task 13: Verify no accidental semicolons or commas crept in
Semicolons aren’t valid separators in SPF. Some humans “format” SPF like a config file.
cr0x@server:~$ dig +short TXT mail.example.com | tr -d '\n' | grep -E '[;,]'
"v=spf1 ip4:203.0.113.10; include:_spf.vendor.example, ~all"
What it means: you have separators that SPF doesn’t understand. Many evaluators return permerror.
Decision: remove punctuation; mechanisms must be space-separated.
Task 14: Check DMARC alignment when SPF “passes” but DMARC fails
cr0x@server:~$ grep -i '^Authentication-Results:' -n sample.eml
45:Authentication-Results: mx.google.com; spf=pass smtp.mailfrom=bounces.vendor-mail.example; dmarc=fail (p=reject) header.from=example.com
What it means: SPF passed for bounces.vendor-mail.example, but DMARC failed because header From is example.com and SPF domain isn’t aligned (and maybe DKIM isn’t aligned either).
Decision: fix alignment (custom MAIL FROM domain for the vendor under your domain, or DKIM alignment), not SPF formatting.
Joke #2: DNS UIs that auto-wrap TXT records are like interns with sudo—sometimes helpful, often confident, and always unsupervised.
Three corporate mini-stories from the trenches
Mini-story 1: The incident caused by a wrong assumption
They were a mid-size company with a normal setup: internal mail, a CRM, and a couple of SaaS tools that sent on their behalf. Marketing wanted a new event platform added before a product launch, because of course they did.
The engineer doing the change assumed the DNS provider wanted the SPF value “exactly as shown in dig.” That’s a reasonable assumption if you’ve spent years looking at zone files and not enough time staring into browser-based DNS editors. They pasted the record including surrounding quotes. The UI accepted it. Nobody yelled. The change went out.
The next morning, deliverability looked… weird. Gmail showed SPF permerror for some recipients, while a security gateway used by partners showed SPF none. The platform vendor insisted their IPs were correct. The internal mail team insisted their outgoing servers were fine. Meanwhile, sales reps were forwarding screenshots of bounces like they were incident reports. They weren’t wrong.
The fix took five minutes once someone pulled the record and noticed the published TXT string literally started with a quote character. The diagnosis took three hours because everyone trusted what the DNS UI preview displayed, and because the team didn’t have a standard “query authoritative and hexdump if necessary” playbook.
The lesson: never trust what the UI renders. Trust what the resolver returns. And don’t accept SPF edits without verifying with a command-line query.
Mini-story 2: The optimization that backfired
A different company had grown their SPF record into a monster: five includes, a couple of mx and a mechanisms “just in case,” and a redirect for legacy mail. They were flirting with the 10-lookup limit. Sometimes it passed; sometimes it permerror’d, depending on DNS timeouts and how receivers expanded includes.
Someone proposed an optimization: “Let’s flatten SPF. We’ll resolve all includes, convert them into ip4: and ip6: entries, and publish one lookup-free record.” On paper, great: faster evaluation, fewer DNS dependencies, less permerror risk.
In practice, their flattening script emitted a long string that the DNS provider UI automatically wrapped into multiple character-strings. The wrap points were arbitrary. One wrap landed between ip4:198.51.100.0/ and 24. Another wrap landed between include: and the domain. A third removed a boundary space.
The record was syntactically broken, and now every receiver saw it. Before the “optimization,” only some receivers hit permerror when lookups timed out. After the “optimization,” the record was plain invalid. Their monitoring (which only checked a single resolver) didn’t catch it because that resolver happened to return a different chunking that masked one of the boundary bugs in their simplistic checker.
They recovered by rolling back and then implementing flattening properly: controlled chunk boundaries, explicit leading spaces on continuation strings, and automated validation against multiple resolvers before publishing. Flattening can work, but it’s a software supply chain now. Treat it like one.
Mini-story 3: The boring but correct practice that saved the day
A finance-adjacent org had a culture of “email auth is production,” which is rarer than it should be. They had a simple practice: any SPF change required a small runbook execution with screenshots banned. Real commands only. They also had a policy: each major sending system gets its own subdomain with its own SPF record.
When procurement forced in a new HR tool that needed sending privileges, the team didn’t touch example.com. They created hrmail.example.com for that system, published a minimal SPF there, and configured the vendor to use it as the bounce domain. The apex SPF stayed stable, and marketing’s chaos stayed quarantined.
Two months later, the vendor rotated their infrastructure and updated their include target. Many customers saw intermittent SPF permerrors due to lookup chains and timeouts. This org didn’t. Why? Their SPF record had only one include and nothing else. Plenty of lookup budget left, and a clean evaluation path.
The boring part—segmentation by subdomain, plus a mandatory command-line validation step—meant the incident never reached their pager. It only reached their weekly “vendor changes” review, where they nodded, updated nothing, and went back to real work.
Common mistakes: symptom → root cause → fix
1) Symptom: SPF permerror “multiple SPF records”
Root cause: More than one TXT record at the same name contains v=spf1 (often created by adding a vendor “SPF record” instead of updating the existing one).
Fix: Consolidate into exactly one SPF policy. Delete extra SPF TXT entries. If you need separate policies, use separate subdomains and configure vendors to use those as MAIL FROM.
2) Symptom: SPF permerror “invalid domain found” or “invalid mechanism”
Root cause: Token welding due to TXT chunk concatenation without a space (e.g., include:_spf.vendor.example~all), or a split mid-token.
Fix: Ensure splits occur at token boundaries and include a leading space in the subsequent chunk. Re-publish and verify with dig showing the intended token separation.
3) Symptom: SPF fails at Gmail but “works” elsewhere
Root cause: Different evaluators handle malformed whitespace/quotes differently; or some pick one SPF record while others return permerror; or DNS inconsistency between authoritative servers.
Fix: Query multiple resolvers and authoritative servers. Remove literal quotes. Eliminate multiple SPF records. Normalize to a single clean string (or carefully split).
4) Symptom: SPF intermittently permerror “too many DNS lookups”
Root cause: Lookup count depends on include expansion order and DNS timeouts; you’re near the 10-lookup ceiling.
Fix: Reduce includes, remove a/mx/ptr, and segment by subdomain. If you flatten, automate it and validate token boundaries and chunking.
5) Symptom: SPF is “neutral” or “none” unexpectedly
Root cause: SPF record not found at the MAIL FROM domain (you updated the wrong name), or record starts with garbage characters due to quoting/paste issues.
Fix: Confirm identity domain from headers. Publish SPF there. Ensure the record starts exactly with v=spf1 (no leading quotes, no BOM, no weird whitespace).
6) Symptom: SPF passes but DMARC fails (and you still land in spam)
Root cause: Domain alignment failure: SPF domain is not aligned with header From, and DKIM isn’t aligned (or absent).
Fix: Configure custom bounce domain under your organizational domain for the vendor, or fix DKIM alignment. Don’t keep “tuning” SPF formatting.
7) Symptom: SPF fails only for IPv6 senders
Root cause: You added ip4: entries but forgot ip6:, or you relied on a/mx mechanisms that don’t cover IPv6 as expected; sometimes combined with split errors around ip6: tokens.
Fix: Add explicit ip6: mechanisms where appropriate, validate record tokenization, and test with an IPv6 sender IP.
Checklists / step-by-step plan
Checklist: publishing an SPF record without breaking it
- Decide the identity domain(s): list every MAIL FROM domain used (apex, marketing subdomain, vendor bounce subdomains).
- One domain, one SPF record: ensure exactly one TXT value containing
v=spf1exists per identity domain. - Write the record in a plain-text editor: no rich text, no ticketing system formatting.
- Keep it simple: prefer explicit
ip4:/ip6:for your own MTAs; useinclude:only when necessary. - Avoid legacy mechanisms: don’t use
ptr. Be cautious withaandmx; they burn lookups and change when DNS changes. - Plan for the 10-lookup limit: count includes and redirects, and assume vendors have their own includes.
- If you must split TXT strings: split only at token boundaries and start continuation strings with a space.
- Publish and verify from authoritative NS: query each authoritative server directly.
- Verify from multiple public resolvers: catch propagation/caching quirks.
- Validate with an SPF evaluator: test at least one known-good sender IP and one known-bad IP.
- Capture evidence: keep the exact record text, query outputs, and the change ticket. You will need it later.
Step-by-step: merging two SPF records safely
- Pull all TXT records and isolate the ones containing
v=spf1. - Copy mechanisms into a single record, preserving order so the most specific matches come earlier.
- Keep exactly one
allmechanism at the end (e.g.,~allor-alldepending on your policy maturity). - Publish the merged record and delete the duplicates.
- Run lookup-count validation and real SPF checks (Task 8/9).
Step-by-step: fixing a broken split TXT record
- Query the record and observe exact chunking (
dig ... +answer). - Concatenate the strings mentally (or with a quick script) and look for welded tokens.
- Edit the record so each continuation string starts with a space unless it continues a token deliberately (rare and risky).
- Re-query authoritative servers and verify both chunking and reconstructed text.
- Re-test SPF evaluation from a known sender IP.
FAQ
1) Do I need quotes around my SPF record in DNS?
Usually no. Quotes are often just how tools display TXT strings. If your DNS UI asks for a value field, enter v=spf1 ... without surrounding quotes unless the UI explicitly documents that it requires them.
2) Why does dig show quotes then?
Because TXT records are strings, and quotes are a safe display convention. They’re not necessarily part of what’s stored as data in the way you think about it.
3) Is splitting an SPF record across multiple TXT strings valid?
Yes, as long as the resulting concatenated text is valid SPF. The trap: concatenation does not insert spaces. If your split removes a space between tokens, you break the record.
4) What’s the simplest “safe” SPF record?
For a domain that sends only through one known source, something like v=spf1 ip4:203.0.113.10 -all is clean and robust. Real environments are messier; the principle is to keep the mechanism set minimal and explicit.
5) Why did SPF start failing after adding one vendor include?
You probably hit the 10 DNS lookup limit, or the new include chain introduced timeouts. Receivers may return permerror when they can’t complete evaluation. Validate lookup counts before and after any include change.
6) Can I publish SPF at the apex and be done?
Only if all your senders use the apex domain as MAIL FROM and you can keep the policy within limits. In practice, using subdomains per vendor is cleaner: separate blast radius, easier changes, fewer accidental conflicts.
7) Why does SPF pass but mail still lands in spam?
SPF is one signal. If DMARC fails (alignment), DKIM is missing/broken, or your sending reputation is poor, you can still land in spam. Don’t confuse “SPF pass” with “deliverability solved.”
8) What does SPF permerror mean operationally?
Your SPF policy is not evaluatable as published (syntax error, multiple records, too many lookups, etc.). Treat it as a production config bug. Fix it like you’d fix a broken firewall rule set: carefully, with validation, and with a rollback plan.
9) Are extra spaces in SPF always fine?
Extra ASCII spaces between tokens usually are. Tabs, non-breaking spaces, and string-splitting that removes required spaces are not. If you’re debugging a “looks fine” record, check the bytes.
10) Should we flatten SPF to avoid lookup limits?
Sometimes. Flattening trades runtime DNS lookups for a maintenance pipeline that must stay correct as vendors rotate IPs. If you flatten, automate updates, validate syntax and chunk boundaries, and monitor for drift. Otherwise you’re replacing one failure mode with a more expensive one.
Next steps you can ship this week
If you only do three things, do these:
- Inventory your MAIL FROM domains from real headers, not tribal memory. Publish SPF where it’s actually evaluated.
- Enforce “one v=spf1 per name” and add a DNS lint check in your change process. Multiple SPF records are a self-own.
- Adopt a verification ritual: query authoritative servers, verify chunk boundaries, and run an SPF evaluation for at least one sender IP before declaring victory.
SPF is not hard. It’s just unforgiving in the places humans like to be casual: whitespace, quoting, and “small edits.” Treat it like production config, because your revenue emails already do.