Why your emails land in spam: a practical SPF, DKIM and DMARC checklist

# email# dns# webdev# security
Why your emails land in spam: a practical SPF, DKIM and DMARC checklistkiwitoolkit

Your app sends a password reset. The user never gets it. Nothing errors on your side: the receiving...

Your app sends a password reset. The user never gets it. Nothing errors on your side: the receiving server simply decided your mail wasn't trustworthy.

Most of the time the cause is three DNS records: SPF, DKIM and DMARC. Here's what each one does and the mistakes that break them.

SPF: who is allowed to send as you

SPF is a TXT record listing the servers allowed to send mail for your domain:

v=spf1 include:_spf.google.com include:sendgrid.net -all
Enter fullscreen mode Exit fullscreen mode

Mistake 1: going over 10 DNS lookups. Each include:, a, mx and redirect costs at least one lookup, and includes can contain more includes. Go over 10 and SPF returns a permerror. Many receivers then treat your mail as failing SPF. This happens slowly as you add tools, so nobody notices.

Fix: remove includes for services you no longer use, and prefer ip4:/ip6: entries where a provider gives you fixed IPs.

Mistake 2: two SPF records. You can have exactly one v=spf1 TXT record. Merge them.

Mistake 3: +all. That allows anyone to send as you. Use -all (fail) or ~all (softfail).

DKIM: proving the message wasn't changed

Your sending service signs every message, and the public key lives at selector._domainkey.yourdomain.com.

Mistake: adding a sending tool and never setting up its DKIM. Every service that sends as your domain (CRM, newsletter, helpdesk, transactional email) needs its own DKIM record. Look up its docs for the selector and CNAME/TXT value.

Also watch for an empty p= in a DKIM record. That means the key was revoked.

DMARC: what to do when checks fail

DMARC ties SPF and DKIM to the visible From: domain and tells receivers what to do with failures:

_dmarc.yourdomain.com  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com"
Enter fullscreen mode Exit fullscreen mode

Mistake: leaving it on p=none forever. none is monitoring only and blocks nothing. The usual rollout:

  1. p=none with rua= reporting turned on. Read the reports for a few weeks.
  2. Fix every legitimate sender that fails.
  3. Move to p=quarantine, then p=reject.

While you're there: TLS and headers

Two more cheap checks: make sure your certificate isn't close to expiry (auto-renewal fails more often than you'd think), and that plain HTTP redirects to HTTPS.

Checking all of it at once

You can check each record with dig:

dig +short TXT yourdomain.com
dig +short TXT _dmarc.yourdomain.com
dig +short TXT google._domainkey.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Or, if you'd rather get one page: I built a free domain report that checks SPF (including the lookup count), DKIM on common selectors, DMARC, TLS and security headers in one pass. There's also an SPF/DKIM/DMARC record builder if you're writing records from scratch. No sign-up, nothing stored.

Checklist

  • [ ] Exactly one SPF record, under 10 lookups, ending in -all or ~all
  • [ ] DKIM set up for every service that sends as your domain
  • [ ] DMARC record exists, with reporting on
  • [ ] A plan to move DMARC from none → quarantine → reject
  • [ ] TLS certificate not expiring soon; HTTP redirects to HTTPS

What's the strangest deliverability problem you've debugged? Tell me in the comments.