Skip to content
Deliverability

Fix SPF, DKIM, and DMARC errors on Cloudflare

Match a DNS symptom to a specific record correction and record what still needs live verification.

THE SHORT ANSWER

Compare your provider’s exact expected record name, type, and value with the authoritative DNS answer. Keep one SPF policy per envelope-sender domain, use DNS-only DKIM CNAMEs, and distinguish published records from authentication and DMARC alignment. A green DNS check does not prove inbox placement.

Before you start #

For
Developers building and operating application email.
Bring
Node.js 22+ and npm for the offline fixtures. Optional real lookups use dig and your provider’s expected records.
Scope
The download runs locally with synthetic data. All email delivery is simulated; no provider credentials or live sends are needed.

Start with the identities in the message #

Write down the visible From domain, envelope-sender domain, DKIM signing domain, and selector before editing DNS. They can be different. SPF checks the envelope-sender identity; DKIM uses its signing domain and selector; DMARC compares authenticated identities with the visible From domain.

The download deliberately uses one synthetic domain to make record-location mistakes easy to see. A real provider may use a bounce subdomain for the envelope sender and several DKIM selectors. Apply its exact instructions for the selected region, not the simplified fixture values.

Confirm that the domain is actually delegated to the nameservers whose records you are editing. Changing a Cloudflare zone that is not authoritative will not change what receiving servers see.

Compare eight synthetic DNS configurations #

Extract the ZIP, then run npm ci, npm test, and npm run demo. The eight fixtures cover expected record locations, duplicate SPF, a proxied DKIM CNAME, a repeated zone name, missing records, normalized hostnames and TXT chunks, duplicate DMARC, and a CNAME/TXT conflict. The tests compare each result with explicit expected findings.

No command in the default test or demo queries DNS or edits a zone. The accompanying worksheet has optional read-only dig commands and space for the UTC check time, authoritative and recursive results, expected values, and the next investigation.

Run from the extracted example foldersh
npm ci
npm test
npm run demo

Read the key part of the example #

This excerpt comes from lab.mjs, lines 46–73, in the download. It shows the central decision; run the complete package with the commands above.

Local simulation · lab.mjsjavascript
  const dkim = at(`${zone.selector}._domainkey.${zone.domain}`).filter((r) =>
    ["TXT", "CNAME"].includes(r.type),
  );
  if (!dkim.length)
    issues.push("No DKIM record at the expected selector hostname.");
  if (dkim.some((r) => r.type === "CNAME" && r.proxied))
    issues.push("DKIM CNAME is proxied: use DNS only.");
  if (
    zone.records.some((r) => r.name.endsWith(`.${zone.domain}.${zone.domain}`))
  )
    issues.push("A hostname repeats the zone: check the record Name field.");
  const dmarc = at(`_dmarc.${zone.domain}`).filter(
    (r) => r.type === "TXT" && /^v=DMARC1;/i.test(r.value),
  );
  if (!dmarc.length) issues.push("No DMARC record at the expected hostname.");
  if (dmarc.length > 1)
    issues.push(
      "Multiple DMARC records: publish one policy at the expected hostname.",
    );
  for (const name of [...new Set(zone.records.map((record) => record.name))]) {
    const records = at(name),
      aliases = records.filter((record) => record.type === "CNAME");
    if (aliases.length && records.length > 1)
      issues.push(
        `CNAME conflicts with another record at ${name}: review the provider’s required record type.`,
      );
  }

Find the files you’ll change #

Pinned dependencies: Node.js built-ins only. Install with npm ci so the lockfile controls the resolved versions.

FilePurpose
lab.mjsThe example behavior shown in this article.
test.mjsAcceptance checks and synthetic failure cases.
demo.mjs / expected-output.jsonA repeatable local experiment and its recorded result.
README.mdSetup commands, expected behavior and production boundaries.
BUILD-BRIEF.mdThe coding-agent brief below.
package.json / package-lock.jsonPinned dependencies and runnable commands.
LICENSEMIT license for adapting this example.

Compare the recorded local result #

Captured from this package’s demo command on October 5, 2026. These results use synthetic fixtures and simulated delivery; they do not measure a live provider or inbox placement.

Recorded local simulation · expected-output.jsonjson
{
  "simulated": true,
  "results": [
    {
      "name": "expected locations",
      "findings": [
        "Fixture has the expected record locations. Authentication and alignment remain unverified."
      ]
    },
    {
      "name": "duplicate SPF",
      "findings": [
        "Multiple SPF records: merge authorized senders into one policy."
      ]
    },
    {
      "name": "proxied DKIM",
      "findings": [
        "DKIM CNAME is proxied: use DNS only."
      ]
    },
    {
      "name": "doubled hostname",
      "findings": [
        "No DKIM record at the expected selector hostname.",
        "A hostname repeats the zone: check the record Name field."
      ]
    },
    {
      "name": "missing records",
      "findings": [
        "No SPF record at the expected envelope-sender domain.",
        "No DKIM record at the expected selector hostname.",
        "No DMARC record at the expected hostname."
      ]
    },
    {
      "name": "case, trailing dot and TXT chunks",
      "findings": [
        "Fixture has the expected record locations. Authentication and alignment remain unverified."
      ]
    },
    {
      "name": "duplicate DMARC",
      "findings": [
        "Multiple DMARC records: publish one policy at the expected hostname."
      ]
    },
    {
      "name": "CNAME and TXT collision",
      "findings": [
        "CNAME conflicts with another record at s1._domainkey.example.test: review the provider’s required record type."
      ]
    }
  ]
}

Fix the record that is wrong #

EvidenceCheckAction
Two TXT records starting v=spf1Both policies apply at the same envelope domain.Merge legitimate senders into one reviewed policy.
DKIM CNAME has proxy enabledThe provider expects DNS delegation to its target.Use DNS only for that CNAME.
selector._domainkey.example.com.example.comThe Name field repeats the zone.Correct the hostname and recheck authoritative DNS.
No _dmarc record at the expected nameThe record may have been added at the wrong host.Compare the exact fully qualified name.

Separate authoritative state from cached state #

First inspect the authoritative nameserver. Then inspect the recursive resolver used by the affected path. An old recursive answer can remain until its cache lifetime ends; a cached negative answer can also outlive the addition of a previously missing record.

Record both answers and their times rather than repeatedly deleting and recreating records. Lowering a TTL after an answer was cached does not retroactively shorten that cached copy’s lifetime. A provider verification job may have its own polling schedule as well.

TXT records do not use Cloudflare’s HTTP proxy. The proxy setting matters for records such as a DKIM CNAME, where the provider needs the DNS delegation to remain visible. Do not treat every Cloudflare DNS issue as a proxy issue.

A correct-looking record is only one part of authentication #

The fixture checker does not expand SPF includes, count DNS-dependent SPF mechanisms, validate DKIM keys, verify a signature, or calculate DMARC alignment. Its clean result explicitly says that authentication and alignment remain unverified.

Inspect Authentication-Results added by a receiving system you trust. An SPF pass for an unrelated domain and a DKIM pass for another unrelated domain do not establish DMARC alignment with your From address. Use the existing authentication guide for identity examples and the header analyzer to inspect reported results.

Read reported authentication results →

Understand SPF, DKIM, and DMARC alignment →

Make a controlled DNS correction #

Save the current record set, identify every legitimate sender affected by a change, and compare the exact provider requirements. Update only the record whose mismatch you have established. Never publish the fixture’s placeholder SPF include or DKIM target.

After the correction, recheck authoritative DNS and the provider’s verification state. Assess authentication from fresh message evidence separately. Do not jump to an enforcing DMARC policy before evaluating legitimate mail streams and the effects on forwarded messages.

If the current incident is an already quarantined message, fixing DNS will not retrieve it. Keep the mailbox investigation moving while you correct the next-send configuration.

Does “DNS only” mean the message will pass DKIM? #

No. It makes the CNAME available as the provider expects. The selector, signing domain, public key and message signature still need to match. Check the provider’s verification state and Authentication-Results from a controlled delivery when you set up production sending.

Use with your coding agent #

Download the example, then copy this brief into your coding agent. The same brief is included as BUILD-BRIEF.md.

Coding-agent build briefmarkdown
# Fix SPF, DKIM, and DMARC errors on Cloudflare — coding-agent brief

## Objective

Explain synthetic SPF, DKIM and DMARC record-location problems without changing real DNS or claiming authentication success.

## Read first

Read README.md, package.json, lab.mjs, test.mjs and demo.mjs. Keep dependency versions pinned to package-lock.json. Use Node.js 22+.

## Dependencies

Node.js built-ins only. Install with npm ci and preserve the lockfile.

## Work

Start at `diagnose` in lab.mjs. Run the existing synthetic fixtures before changing behavior. Preserve the guide's original scenario and add a regression check for each changed failure case.

## Constraints

All email is simulated. Do not add provider credentials, send email, deploy services, or use the application's database. Preserve unknown outcomes instead of claiming delivery. Do not turn the local demonstration into production authentication or a public mail relay. Explain the production setup separately.

## Verification

Run `npm ci`, `npm test`, and `npm run demo`.

## Acceptance

Every fixture must match its expected explanation, including TXT chunks, hostname casing, duplicate policies and CNAME collisions. Reject malformed fixtures. Preserve the warning that record shape cannot prove SPF evaluation or DMARC alignment.
No real tokens, signing secrets, or recipient data appear in logs. All fixtures use synthetic values.

Check the download #

The ZIP contains 7,678 bytes. Compare its SHA-256 digest with this value before extracting. Downloads are free and require no signup.

SHA-256 · cloudflare-email-dns-troubleshooting.ziptext
3e993e4e90f757eb2c5749d0072db56d2e441d8e8734e6208e700a9b5bf420ea

Sources and further reading

Written for Stampwing with AI assistance and checked against the linked documentation. Examples are educational; simulated results are labelled. How these resources are maintained.