Build1 publisher2 min readPublished
Nested includes push GitHub's SPF record to exactly 10 of its 10 allowed DNS lookups
GitHub's SPF record uses all 10 DNS lookups RFC 7208 allows once the includes inside its vendors' records are counted. One more mail vendor would make SPF return PermError and leave GitHub's mail to pass DMARC on DKIM alone.
The Engineer · Build desk

What happened
- GitHub's SPF record, as read on 28 September 2026, holds eight vendor includes, Salesforce's and SendGrid's among them, plus five fixed IPv4 entries.
- The author of a free SPF checker says its first version reported GitHub at 8 of 10 because it counted only the terms in GitHub's own record.
- The two lookups the checker missed sit one level down, one inside Salesforce's SPF record and one inside SendGrid's.
- RFC 7208 section 4.6.4 applies the cap per evaluation, so every record reached through an include, at any depth, draws on the same budget of 10.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure With zero headroom, one include added inside any of the eight vendor records puts GitHub past the cap, and GitHub does not have to touch its own DNS for that to happen.
- decision Dropping a mail vendor now needs a DNS cleanup step, because an include that points at a vendor whose SPF record was deleted is itself a PermError under section 5.2.
- constraint A checker that counts only a domain's own terms overstates headroom, so its figure cannot be used to decide whether a new mail vendor fits under the cap.
In the author's corrected code, an include: or a redirect= costs one lookup plus whatever the target record costs. The a, mx, ptr and exists: terms cost one each. The ip4, ip6, all and exp= terms cost nothing [13]. GitHub's five ip4 terms are free under those rules, so all ten of its lookups come from the eight includes and the records behind them [3].
The lookup inside Salesforce's record is an exists: term with a %{i} macro in it. The macro expands to the connecting IP address. Salesforce's record therefore makes a live DNS query for every message, and that query counts against the limit like any other [11].
The first version's help text admitted it could not see inside the includes [8]. The author wrote that "a number on a result card beats a caveat in a paragraph every time" [9]. I agree, and I'd go further. A checker that cannot walk the tree should not print a count at all.
The replacement is about 30 lines. It queries Google's DNS-over-HTTPS JSON API, so it needs no DNS library and runs in Node 18 or later, or in a browser. Run against github.com, it returns 10 [14]. It is careful work for its size, because each branch encodes a separate RFC 7208 rule. Loops are tracked per path, not globally, so two vendors that include the same third record are both charged for it [15]. A redirect= is ignored when the record already has an all term, per section 6.1, and counting it anyway inflates the total [18]. Two SPF records on one name is a PermError under section 4.5. The code throws on that case instead of using the first record [17].
The author says each of those rules is a bug they have seen in real checkers [19]. The post does not name those checkers. It measures a single domain, so it cannot say how many other domains sit as close to the ceiling as GitHub does [19].
The sketch also skips one rule, by the author's choice. That is the void-lookup limit: lookups that return NXDOMAIN or an empty answer should be capped at two [20].
What to watch
- A new include: inside any of the eight vendor records GitHub references would put github.com at 11 lookups and return PermError.
- GitHub removing an include, or swapping one for ip4 ranges that cost no lookups, would restore headroom under the cap.
- The author adding the void-lookup limit of two to the checker would close the one RFC rule the published sketch leaves out.