Sender Policy Framework, also known as SPF, is a value published on the DNS that generally includes the list of IP addresses that are permitted to send emails for a domain. The domain owner publishes a text (TXT) record at the domain apex, and the receiver mail server queries that record and then checks the IP address of the connecting client against it during the SMTP transaction.

An SPF record is a list of mechanisms that are evaluated from left to right, with the first match deciding the outcome. For instance, the record below allows Maileroo, Google Workspace, Microsoft 365 and an IP range (/24) to send emails on behalf of the domain.

v=spf1 include:_spf.maileroo.com include:_spf.google.com include:spf.protection.outlook.com ip4:203.0.113.0/24 ~all

The ip4 and ip6 match the address literally, so no extra lookups are required. However, include mechanism requires additional work which entails querying another domain's policy and evaluating it recursively.

The 10 Lookup Problem

RFC 7208 4.6.4 restricts the total number of lookups to 10, this is a limit set to prevent SPF being used to amplify DNS traffic. Exceeding this limit usually results in a PermError and DMARC treats that as an SPF failure.

For enterprise businesses, this cap is very easy to hit as they may use multiple software at the same time. For instance, one of our customers uses Maileroo, Mandrill, Amazon SES, MailChimp, Zendesk, Salesforce, Microsoft 365 and ServiceNow at the same time, which means they hit these limits very easily. Each of those includes performs another one or two nested lookups of its own, which is how a record with only four or five includes ends up over the limit.

To address this issue, there are two mechanisms that we can use, SPF Flattening and SPF Macros, which we are going to discuss now.

How Flattening Works

SPF Flattening essentially involves resolving the complete include tree and then creating a list of all the IP addresses used, and publishing them as ip4 and ip6 mechanisms, at zero lookup cost.

However, RFC 7208 3.4 recommends keeping the record size under 512 bytes for the whole DNS response which is a classic UDP payload limit, so creating a massive flattened SPF record is not ideal either. Also, generally speaking, RFC 1035 3.3 also restricts the size of a TXT record to 255 characters per chunk. A large TXT record such as a DKIM key can be represented as two or more chunks which are merged by the evaluator into one single, continuous string.

Anyway, to resolve this issue, you can still use include in your flattened record to include more IP addresses.

Consider the example below which includes the following TXT record at the apex of the domain.

v=spf1 include:_spf1.sadtvt.spf.zenspf.com ~all

Here _spf1.sadtvt.spf.zenspf.com resolves to the following:

v=spf1 ip4:23.249.208.0/20 ip4:23.251.224.0/19 ip4:24.110.64.0/18 ip4:45.14.148.0/22 ip4:49.13.248.208/28 ip4:50.31.32.0/19 ip4:50.31.156.96/27 ip4:50.31.205.0/24 include:_spf2.sadtvt.spf.zenspf.com

And _spf2.sadtvt.spf.zenspf.com uses the following:

v=spf1 ip4:54.240.0.0/17 ip4:69.72.32.0/20 ip4:69.169.224.0/20 ip4:76.223.128.0/19 ip4:76.223.176.0/20 ip4:77.32.128.0/18 ip4:77.32.192.0/19 ip4:85.204.106.0/24 ip4:87.253.232.0/21 ip4:94.143.16.0/21 include:_spf3.sadtvt.spf.zenspf.com

Essentially, what happens is that each record links to the next flattened record to keep under the 512 bytes limit. With this approach, you still need to stay under the 10 lookup limit, but in most cases, this is enough and works well for majority of the businesses. But sometimes, the number of IP addresses is so large that you can't really fit them properly even with linked SPF flattened records, and this is where SPF Macro comes in. Another issue is that flattened SPF records can become stale over time as companies can add or remove IP addresses, which is quite frequent with large providers such as Microsoft and Google.

How Macros Work

SPF Macros solve the same problem, but a little bit differently. Instead of publishing the IP address list, you publish a single mechanism which allocates the responsibility to an authoritative nameserver, and requires an SPF evaluator to ask a question about the connecting IP address to the nameserver whether the IP address is allowed to send on behalf of that domain or not.

This mechanism is called exists, and the macro is %{i}. When a receiver evaluates the record, it expands %{i} into the IP address of the connecting client, and then performs an A lookup on the resulting name. If the lookup returns any answer, the mechanism matches. If it returns NXDOMAIN, evaluation moves on to the next term.

So a macro record looks like this, regardless of how many senders sit behind it:

v=spf1 exists:%{i}.sadtvt.spf.zenspf.com ~all

A connection from 188.241.220.6 causes the receiver to query:

188.241.220.6.sadtvt.spf.zenspf.com

For example, if we use dig to resolve the A record, you can clearly see that it responds with a dummy 127.0.0.1 for that hostname, which indicates the IP address is allowed to send emails on behalf of that domain.

dig A 188.241.220.6.ip.lzgpse.spf.zenspf.com

; <<>> DiG 9.16.1-Ubuntu <<>> A 188.241.220.6.ip.lzgpse.spf.zenspf.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48795
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;188.241.220.6.ip.lzgpse.spf.zenspf.com.        IN A

;; ANSWER SECTION:
188.241.220.6.ip.lzgpse.spf.zenspf.com. 266 IN A 127.0.0.1

;; Query time: 4 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Sun Sep 13 13:34:27 AEST 2026
;; MSG SIZE  rcvd: 83

On the other hand, querying 1.1.1.1 returns a NXDOMAIN which means that IP address is not allowed to send emails.

dig A 1.1.1.1.ip.lzgpse.spf.zenspf.com

; <<>> DiG 9.16.1-Ubuntu <<>> A 1.1.1.1.ip.lzgpse.spf.zenspf.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 14548
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;1.1.1.1.ip.lzgpse.spf.zenspf.com. IN   A

;; AUTHORITY SECTION:
zenspf.com.             300     IN      SOA     ns1.zenspf.com. hostmaster.zenspf.com. 1 7200 3600 1209600 300

;; Query time: 556 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sun Sep 13 13:38:59 AEST 2026
;; MSG SIZE  rcvd: 112 

The nameserver behind spf.zenspf.com is not a typical DNS zone and does not serve a static set of records. It parses the IP out of the query name, checks it against its database of authorized ranges for that domain, and returns an A record if it is a member or NXDOMAIN if it is not. The membership test happens at query time, so the answer is never stale.

The cost is one lookup. Not one per sender, one total. The record is also a fixed length forever, which means neither the 512-byte response limit nor the 255-byte string limit is ever in play, no matter how many providers you add.

Obviously, using an SPF Macro is much more complex, but works better at scale, and once implemented, it requires little to no attention.

So Which Should You Use?

Both methods keep you under the lookup limit, so the choice ultimately comes down to the size of your address set.

If the chain alone exceeds ten lookups, flattening is no longer viable and macros become mandatory. The other issue is upkeep: a record flattened by hand, or by a script you run manually, is accurate only until a provider changes its sending addresses, and providers rarely announce that.

On the other hand, using an SPF Macro would require you to build your own custom DNS server and ideally operate it over a globally redundant infrastructure which can prove complex or overwhelming for small businesses.

Either way, you want this automated. So we built ZenSPF for our customers.

Introducing ZenSPF by Maileroo

ZenSPF by Maileroo handles both approaches: it resolves your include tree, publishes chained flattened records while they fit under the lookup limit, and moves to macro mode backed by a dynamic authoritative nameserver once the address set outgrows chaining. Re-resolution every 30 minutes rather than when someone remembers to, so a provider rotating its ranges does not quietly break the record.

Setup takes under five minutes and needs nothing but a Maileroo account. It is free, and it serves your record from the nearest point of presence on our anycast network at 99.99% uptime.

Flattened SPF record generated by ZenSPF
Flattened SPF record generated by ZenSPF

Wrapping Up

The SPF lookup limit was written to stop DNS amplification attacks, but it has also aged badly, and limits legitimate users and businesses from having many email-based software or applications. Flattening and macros are both excellent ways to address the limit. While flattening resolves the records into a static list of IP addresses, a macro-based solution publishes a single term and moves that list behind a nameserver, which answers each query from a set kept current on the provider's side. The record itself stays slim.