# SPF Record Deep Dive — Mechanisms, Limits, and Edge Cases

> SPF looks simple: list your sending IPs. The real-world version: 10-lookup limits, include-chains, the difference between -all and ~all, SPF flattening at scale. This is the operator's deep dive.

- Canonical URL: https://acellesend.com/kb/articles/spf-record-deep-dive
- Author: Lena Hart, Email Deliverability Editor
- Category: DNS & Domain Setup — SPF, DKIM, DMARC, BIMI, domain verification, MX records, Return-Path
- Content type: tutorial · Difficulty: advanced
- Tags: Acellemail
- Published: 2026-07-04 · Last updated: 2026-08-15
- Reading time: 7 min
- Publisher: AcelleMail Knowledge Base, https://acellesend.com/kb

---

## SPF in one line

SPF tells receiving servers which IPs (or which other domains' SPF records) are authorized to send from your domain. Published as a TXT record at the apex domain.

```
v=spf1 include:amazonses.com -all
```

Translation: "Allow Amazon SES to send for me; REJECT everything else."

## The mechanisms

| Mechanism | Meaning | Example |
|---|---|---|
| `ip4:1.2.3.4` | Authorize this specific IPv4 | `ip4:54.240.0.10` |
| `ip4:1.2.3.0/24` | Authorize this CIDR | `ip4:1.2.3.0/24` |
| `ip6:...` | Authorize IPv6 | `ip6:2001:db8::1` |
| `include:other.com` | Pull in other.com's SPF record | `include:amazonses.com` |
| `a` | Authorize this domain's A record IP | `a` (the domain's own A) |
| `a:mail.brand.com` | Authorize a specific subdomain's A record | `a:mail.brand.com` |
| `mx` | Authorize the MX records' IPs | `mx` |
| `exists:%{i}.spamcheck.brand.com` | Macro-based check (rarely used) | (advanced) |

## The qualifiers (before each mechanism)

| Qualifier | Action |
|---|---|
| `+` (or none) | PASS — authorized |
| `~` | SOFTFAIL — receiver-discretion (typically goes to spam) |
| `-` | FAIL — REJECT |
| `?` | NEUTRAL — no policy stated |

The `all` at the end is the catch-all: `-all` (hard fail) is the most strict; `~all` (soft fail) is conservative for new setups.

```
v=spf1 ip4:1.2.3.4 ~all
# Allows 1.2.3.4; soft-fail everything else (likely spam folder)

v=spf1 ip4:1.2.3.4 -all
# Allows 1.2.3.4; HARD-FAIL everything else (rejected)
```

Use `~all` until production stable, then switch to `-all`.

## The 10-lookup limit

SPF lookups are recursive: `include:amazonses.com` triggers a DNS lookup for amazonses.com's SPF record, which may include further records, recursively.

RFC 7208 caps recursive lookups at **10 per evaluation**. Beyond 10, receivers return `PermError` and the SPF check fails.

Each of these counts as 1 lookup:

- Every `include:` (and the includes inside it count too, recursively)
- Every `a` and `mx`
- Every `ptr` (deprecated)
- Every `exists`

`ip4:` and `ip6:` do NOT count (no DNS lookup needed).

Multi-vendor blowup example:

```
v=spf1 include:amazonses.com include:_spf.mailgun.org include:sendgrid.net include:spf.mtasv.net -all
```

Each include resolves to ~3-4 sub-includes:

- `amazonses.com` → 1 (amazonses.com itself) + 1 (sub-include) = 2 lookups
- `_spf.mailgun.org` → 1 + 2 = 3 lookups
- `sendgrid.net` → 1 + 4 = 5 lookups
- `spf.mtasv.net` → 1 + 1 = 2 lookups

Total: 2 + 3 + 5 + 2 = 12 lookups → SPF PermError → ALL your mail fails SPF check.

## SPF flattening (the fix at scale)

Flattening = pre-resolve all includes + replace with IPs in your published record:

```
# Before (might exceed 10):
v=spf1 include:amazonses.com include:_spf.mailgun.org include:sendgrid.net -all

# After (flattened):
v=spf1 ip4:54.240.0.0/18 ip4:13.32.0.0/15 ip4:54.240.36.0/22 ip4:104.197.0.0/16 ... -all
```

Trade-off:

- ✅ No 10-lookup issue
- ✅ Faster receiver-side check
- ❌ When vendors update IP ranges, your flattened record becomes stale
- ❌ Without automated re-flattening, you'll silently fail SPF when ranges shift

Tools that auto-flatten daily:

- **ValiMail SPF** — paid; handles re-publishing via your DNS host's API
- **EasySPF** — paid; similar
- **DMARCLY SPF** — included in their DMARC platform
- **Manual script + cron** — DIY, possible but high-maintenance

For 1-2 vendor setups, manual is fine. 3+ vendors, use a tool.

## Verify in AcelleMail
### Open the sending-domain detail

In AcelleMail's sidebar, **Sending → Sending domains**. The list shows every domain you've registered with status chips (Verified / Pending / Failed) and per-auth indicators:

![Sending domains list](https://acellesend.com/plugins/acelle/landing/images/help/kb/sending-servers/17-sending-domains-list.png)

Click into your domain row. The detail page surfaces exactly which DNS records to publish (TXT for SPF, CNAMEs for DKIM, TXT for DMARC) with copy-paste-ready values + current verification state per check:

![Sending-domain detail — DNS records + auth status](https://acellesend.com/plugins/acelle/landing/images/help/kb/sending-servers/18-sending-domain-detail.png)

The DKIM CNAME and DMARC TXT records are visible alongside SPF. The detail page shows live verification status — re-run with the *Verify domain* button after publishing changes.

## Common SPF errors + fixes

| Error | Cause | Fix |
|---|---|---|
| **PermError** (10-lookup exceeded) | Too many includes | Flatten via tool; OR remove unused vendor includes |
| **TempError** (DNS timeout) | DNS resolver couldn't reach one of the includes | Usually self-resolves; persistent = check vendor's DNS health |
| **Fail / Hard fail** | Sending IP not in your SPF record | Add the vendor's `include:` OR `ip4:` |
| **None** (no SPF record published) | Missing TXT record | Publish per [Complete DNS setup](https://acellesend.com/kb/articles/complete-dns-setup-for-email-sending) |
| **Softfail** intentional but mail goes to spam | Some receivers treat `~all` as suspicious | Switch to `-all` after production-stable |
| **Pass** but DMARC still fails | SPF + DKIM + DMARC alignment issue | Check envelope-sender domain matches From: |

## Real-world SPF records

```
# Small sender, single vendor
v=spf1 include:amazonses.com -all

# Multi-vendor mid-size
v=spf1 include:amazonses.com include:_spf.mailgun.org -all

# Large enterprise with corporate-mail mixed in
v=spf1 include:_spf.google.com include:amazonses.com include:_spf.mailgun.org include:sendgrid.net -all
# (4 includes — close to 10-lookup limit; consider flattening)

# Self-hosted Postfix on a fixed IP
v=spf1 ip4:1.2.3.4 -all

# Hybrid: self-hosted + transactional via SES
v=spf1 ip4:1.2.3.4 include:amazonses.com -all
```

## What SPF doesn't do

SPF alone is NOT sufficient for deliverability:

| Concern | SPF's role | Other records |
|---|---|---|
| Sender authentication | ✅ (per-IP) | DKIM (per-message), DMARC (alignment) |
| Message integrity | ❌ | DKIM (cryptographic signature) |
| Phishing protection | Partial | DKIM + DMARC together |
| BIMI logo display | Required base | DKIM + DMARC + BIMI |

SPF is necessary but not sufficient. Always pair with DKIM + DMARC.

<details>
<summary><strong>Advanced: SPF macro syntax + multi-domain alignment + per-vendor flattening tools</strong></summary>

**SPF macros (rarely needed, mentioned for completeness):**

```
v=spf1 exists:%{i}.spamcheck.brand.com -all
```

Macro `%{i}` expands to the sender IP. The `exists` mechanism checks if that DNS name resolves. Used for per-IP spam-list integration. Almost never needed for AcelleMail use cases.

Other macros:

- `%{s}` — sender (envelope-sender)
- `%{l}` — local-part of envelope-sender
- `%{d}` — domain
- `%{p}` — sender's PTR record (deprecated due to performance)
- `%{h}` — HELO domain

**Multi-domain alignment (for subdomain senders):**

```
Apex (brand.com):       v=spf1 include:amazonses.com -all
Subdomain (mail.brand.com): v=spf1 include:amazonses.com -all
```

Each subdomain needs its own SPF record. Don't rely on the apex to apply to subdomains automatically (DNS doesn't work that way for TXT records).

**Per-vendor SPF flattening tools:**

```
ValiMail SPF       — $50-100/month; vendor-agnostic
EasySPF            — $20-50/month; basic
DMARCLY SPF        — included with DMARC tier
PowerDMARC SPF     — included with DMARC tier
```

For self-managed flattening, the script:

```bash
#!/bin/bash
# Daily SPF flatten + republish
# Replace IP ranges from vendor's published SPF includes

current_spf=$(dig TXT yourdomain.com +short | grep "v=spf1")
new_includes=""

for vendor in amazonses.com _spf.mailgun.org sendgrid.net; do
  ips=$(dig TXT $vendor +short | grep -oE 'ip4:[0-9./]+|ip6:[0-9a-f:/]+')
  new_includes="$new_includes $ips"
done

new_spf="v=spf1$new_includes -all"
echo "Updating SPF: $new_spf"

# Update via Cloudflare API (adjust per your DNS host)
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
  -H "Authorization: Bearer $CF_TOKEN" \
  -d "{\"type\":\"TXT\",\"name\":\"yourdomain.com\",\"content\":\"$new_spf\"}"
```

Run as a daily cron. Manual fallback when tool subscriptions feel expensive.

**SPF compliance audit:**

```bash
# Daily check on all your sending domains
domains=$(curl -sH "Authorization: Bearer $TOKEN" \
  "https://acellemail.com/api/v1/admin/sending-domains" | jq -r '.data[].name')

for d in $domains; do
  spf=$(dig TXT $d +short | grep "v=spf1")
  if [ -z "$spf" ]; then
    echo "🚨 $d: SPF MISSING"
    continue
  fi
  lookups=$(echo $spf | grep -oE "include:|a:|mx" | wc -l)
  if [ $lookups -gt 8 ]; then
    echo "⚠️ $d: high lookup count ($lookups) — risk of PermError"
  fi
done
```

**SPF macros for compliance reporting:**

For per-customer SPF visibility in SaaS context:

```
v=spf1 include:_customer.%{d}.spf.acellemail.com -all
```

Per-customer SPF macro that resolves dynamically. Advanced; typically operator-managed by AcelleMail SaaS providers.

</details>

## Related articles

- [How to set up SPF, DKIM, and DMARC records](https://acellesend.com/kb/articles/how-to-set-up-spf-dkim-and-dmarc-records)
- [Complete DNS setup for email sending](https://acellesend.com/kb/articles/complete-dns-setup-for-email-sending)
- [DMARC enforcement migration](https://acellesend.com/kb/articles/dmarc-enforcement-migration)
- [Return-path and envelope-sender explained](https://acellesend.com/kb/articles/return-path-and-envelope-sender-explained)

---

## Related articles

- [BIMI with VMC — Cost, Process, and Whether It's Worth It](https://acellesend.com/kb/articles/bimi-with-vmc-cost-walkthrough): VMC (Verified Mark Certificate) costs $1,500/yr per domain and takes 4-8 weeks to obtain. This guide walks the cost math, the trademark requirements, the alternative paths, and a decision framework for whether VMC is right for your sending program.
- [Return-Path vs Envelope-Sender — What Each Does in AcelleMail](https://acellesend.com/kb/articles/return-path-and-envelope-sender-explained): The "From" address recipients see isn't the address that handles bounces. That's the envelope-sender (also called the return-path or MAIL FROM). This guide separates the three sender concepts and shows where AcelleMail configures each.
- [My Domain Shows "Pending" — DNS Propagation Explained](https://acellesend.com/kb/articles/dns-propagation-explained): You published your SPF/DKIM/DMARC records but AcelleMail still shows the domain as Pending. That's almost always normal DNS propagation. Here's what's happening and exactly what to do.
- [How to Set Up SPF, DKIM, and DMARC Records — The Basics](https://acellesend.com/kb/articles/how-to-set-up-spf-dkim-and-dmarc-records): New to email sending? These are the 3 DNS records you absolutely need before sending a single campaign. This guide is the basics — what each does, what to publish, how AcelleMail verifies them.
- [Custom Tracking Domain for Click URLs — AcelleMail Setup](https://acellesend.com/kb/articles/custom-tracking-domain-for-click-urls): By default every link in your emails routes through acellemail.com's tracking redirect. Replace with click.yourdomain.com — same redirect mechanics, your own brand. This guide walks the setup + the CNAME + the link-report verification.
