Domain roles: what is this domain trying to be?
Every report names the domain's role, detected from two measured
axes: does it run email (usable MX records — or an RFC 7505
null MX / v=spf1 -all declaring the opposite), and what does its
web root do — serve a site, permanently redirect
(301/308) to a different domain, or answer nothing. The two answers
name one of six roles:
| What's set up | Role |
|---|---|
| Email + web | full-service |
| Email + redirect | redirect-service |
| Email only | email-only |
| Web only | web-only |
| Redirect only | redirect-only |
| Neither | no-service |
The fine print, exactly: example.com landing on
www.example.com (or the reverse) is canonicalization, not a redirect —
www is a spelling of the same site. Landing anywhere else is a
redirect role, even inside the same domain: example.com forwarding to
blog.example.com forwards. A registrar sale-lander is a website, so it
reads as web-only. When signals contradict or can't be measured, the report says
unclassified rather than guessing.
The role changes how findings read: "no DKIM selectors found" is a caution on a mail
domain and expected on a no-mail one, so statuses are adjudicated
role-relatively — every adjustment is listed in the report, with the raw
status preserved. Know what the domain is supposed to be? Set the
intended use (the goal parameter) and the report is
judged against that instead, with contradictions surfaced as gaps — "goal
email-only, but no MX records exist."
Apex + subdomains: every report anchors at the apex
Registration, MX records, SPF, and DMARC governance belong to the
registrable domain — the apex, example.com — so that's
what every report is anchored to, whatever you type.
www.example.com simply folds into example.com: www is a
spelling, not a different place, and the report notes the fold.
Check any other subdomain — blog.example.com — and you get the
apex report plus a lens on the name you asked about: what its web root
does, and whether it runs any mail setup of its own. Most subdomains don't, and
that's healthy — their mail posture is governed by the apex's DMARC (the
sp= tag when present, otherwise p=), so the lens says so
in one line instead of raising alarms about records that were never supposed to
exist. A subdomain with its own MX, SPF, DKIM, or DMARC is usually
deliberate — newsletters, ticketing, transactional mail — so the lens surfaces it
and asks you to confirm it's yours.
One thing always carries down: a registry alarm on the apex — suspended, past expiry, expiring soon — takes every subdomain with it, so it shows at full volume on both reports.
Why the split? Each thing the report measures is owned by a different scope — the web's unit is the origin, mail records hang off individual DNS names, and registration belongs to the apex:
| Concept | Scope | Why |
|---|---|---|
| security.txt (RFC 9116) | Per-origin | every https host answers for itself; a redirect can hand the question to the destination site |
| Website / redirect | Per-name | each DNS name serves, forwards, or stays silent on its own |
| Receiving mail (MX) | Per-name | any name can accept mail — subdomains included, which is why the lens looks |
| SPF | Per-name | authorizes senders for exactly the name in the return path; a sending subdomain needs its own |
| DKIM | Per-signing-domain | the signature's d= tag names the domain that vouches; keys live under that name |
| DMARC | Apex, inherited | the organizational domain's policy covers every subdomain (sp=, else p=) unless one publishes its own |
| Registration & expiry | Apex only | you register example.com — blog.example.com was never registered at all |
| Lifecycle alarms | Apex only | a suspended or expired apex takes every subdomain down with it |
The 10-lookup limit, exactly
RFC 7208 §4.6.4 allows an SPF evaluation at most 10 DNS-querying terms.
Cross it and the receiver returns permerror — the record is treated as
unusable, as if you had published nothing. The rule is simple; how the ten are
counted is where records quietly go wrong.
What costs a lookup
Six terms query DNS and count: include, a, mx,
ptr, exists, and the redirect modifier.
Three are free: ip4, ip6, and all — they
compare against data already in hand. (mx has a further sub-limit of
10 address lookups per name, and ptr is deprecated outright.)
Duplicates are never flattened
The limit counts terms evaluated, not unique names. Every time evaluation
reaches include:d.example, the receiver runs a fresh
check_host() on d.example's record — and every
DNS-querying term inside it counts again. The resolver's cache makes the
repeat queries fast; the counter does not care. Nothing in RFC 7208 deduplicates,
merges, or flattens repeated targets.
The classic shape is the diamond: two branches that both include the same target.
example.com v=spf1 include:b.example include:c.example -all
b.example v=spf1 include:d.example -all
c.example v=spf1 include:d.example -all
d.example v=spf1 a mx ptr exists:chk.example -all | lookup | term evaluated | via |
|---|---|---|
| 1 | include:b.example | example.com |
| 2 | include:d.example | b.example |
| 3–6 | a · mx · ptr · exists | d.example, first evaluation |
| 7 | include:c.example | example.com |
| 8 | include:d.example | c.example |
| 9–12 | a · mx · ptr · exists | d.example, evaluated again |
One nuance keeps the number honest in both directions: receivers evaluate left to right and stop at the first mechanism that matches, so a given message may cost fewer than the worst case. But which path a message takes depends on the sender's IP — a checker has to count the path where nothing matches until the end, because some real message will take it.
A diamond is not a loop
Reaching the same target through two different branches is legal. What is not
legal is the path revisiting itself — a includes
b, which includes a again. In practice a loop never
recurses forever: each round trip burns lookups, so the evaluation crosses the
10-lookup limit and dies as permerror within a couple of laps. A
checker that confuses the two shapes will call your legal diamond a loop — or
miss counting the diamond twice.
How the spf check counts
- worst case, per edge
- Every DNS-querying term counts each time evaluation would reach it. A diamond's shared target counts once per branch — never flattened.
- fetched once
- Records are cached for the walk, so the diamond's shared target is fetched from DNS once even though its terms count twice. Counting and traffic are separate questions.
- loops = path only
- A loop is flagged only when an include/redirect chain revisits a domain already on the current path. Diamonds pass.
- dead includes
-
An
includewhose target publishes no SPF record is its own permerror (RFC 7208 §5.2) and is reported separately. - 5-second budget
- Slow or black-holed include targets can stall the walk, so counting carries a wall-clock budget. If it expires, the report says so and gives the count as "at least N" — a partial count is stated as partial, never rounded up to a verdict.
Getting back under ten
Flatten what you can: replace an include whose contents you control
with its ip4/ip6 networks (they cost nothing), and keep
a note to re-check them — flattened records go stale silently. Drop vendors you no
longer send through, remove ptr, and consider moving bulk senders to
a subdomain with its own record. Then
run the check — the report counts your record's worst case the way receivers will, and tells you
exactly what to change.