DEV Community

bbu99054-a11y
bbu99054-a11y

Posted on

Why include:mailgun.org actually costs 5 SPF lookups (and broke our email)

If you manage DNS for any web app or SaaS, your SPF record probably looks innocent enough:

v=spf1 include:_spf.google.com include:mailgun.org include:sendgrid.net ~all

Count the tokens: Google (1), Mailgun (1), SendGrid (1). That's 3 lookups.
Well within the RFC 7208 limit of 10, right?

Except it's actually 8 lookups.

Add just one more service—say, a sales tool like Klaviyo or support mail via Zoho—and your entire SPF setup silently fails with a PermError. Inbound servers stop evaluating, DMARC alignment breaks, and transactional emails start landing in spam.

Here is what is actually happening under the hood.

The 10-Lookup Rule is Recursive
RFC 7208 §4.6.4 limits DNS lookups to 10 to prevent mail servers from being abused as DDoS amplification vectors.

Most people assume each include: directive in their DNS dashboard equals 1 lookup. But receiving mail transfer agents (MTAs) don't stop at the first TXT record—they recursively follow every nested include until they hit raw IP addresses.

And some popular email providers nest much deeper than you'd expect.

1. Mailgun (mailgun.org) = 5 Lookups
When you add include:mailgun.org, here is the actual DNS resolution path that the receiving server has to walk:

  1. mailgun.org
    └── points to: include:_spf.mailgun.org include:_spf.eu.mailgun.org (Lookup 1)

  2. _spf.mailgun.org
    └── points to: include:_spf1.mailgun.org include:_spf2.mailgun.org (Lookup 2)

  3. _spf.eu.mailgun.org
    └── resolves EU pool IPs (Lookup 3)

  4. _spf1.mailgun.org
    └── resolves US pool 1 IPs (Lookup 4)

  5. _spf2.mailgun.org
    └── resolves US pool 2 IPs (Lookup 5)
    One line in your DNS record just burned 50% of your global lookup budget.

2. Zoho Mail (zoho.com) = 5 Lookups
Zoho does something similar with its regional clusters:

  1. zoho.com
    └── points to: include:spf.zoho.com (Lookup 1)

  2. spf.zoho.com
    └── contains 4 regional cluster includes:
    ├── include:spf1.zoho.com (Lookup 2)
    ├── include:spf2.zoho.com (Lookup 3)
    ├── include:spf3.zoho.com (Lookup 4)
    └── include:spf4.zoho.com (Lookup 5)
    Again, 5 lookups for a single provider.

The Real Problem: Silent Failures
If you combine:

Google Workspace: 1 lookup (_spf.google.com)
SendGrid: 2 lookups (sendgrid.net → ab.sendgrid.net)
Mailgun: 5 lookups
You are at 8/10.

If anyone on your marketing or sales team adds Klaviyo (3 lookups) or Zoho (5 lookups) to the root domain: 8 + 3 = 11 lookups

The moment a DNS query count hits 11, the receiving server (Google, Microsoft, Yahoo) immediately throws PermError and aborts.

Your DNS dashboard (Cloudflare, Route53, Namecheap) will show a green checkmark because the syntax itself is valid. But inbound servers will treat SPF as a complete failure.

How to Fix It

  1. Stop putting every service on the apex domain The cleanest solution is subdomain isolation:

Keep corporate mail on the root domain: example.com (include:_spf.google.com ~all)
Move transactional mail to a subdomain: mail.example.com (include:mailgun.org ~all)
Move marketing newsletters to another: news.example.com (include:sendgrid.net ~all)
Each subdomain gets its own fresh 10-lookup quota.

  1. Check your actual recursive count Don't trust tools that just count the number of words in your TXT string. You need to trace the actual recursion tree.

I got tired of manually running dig chains across DNS providers, so I put together a quick client-side tool to do the recursive walk in-browser: 👉 https://tools.postsyncapp.com

It queries Cloudflare and Google DoH (DNS-over-HTTPS) APIs directly from your browser, counts the real nested lookups, and flags if you're close to the PermError line. No signup required and zero backend storage.

If you haven't checked your root domain's SPF lookup count recently, trace it. You might be closer to 10 than you think.

Top comments (0)