Email Marketing

Cold Email for SaaS: 9 Deliverability Checks That Land

9 cold email deliverability checks tested on real SaaS domains. SPF, DKIM, DMARC, warmup, hygiene — here is what moved inbox rate, with code and tradeoffs.

Huifer
Huifer
May 13, 20269 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. I run cold email outbound for two SaaS products from three production sending domains, monthly volumes 1,200 / 2,800 / 4,500. Cold email SaaS deliverability is what I tested for this post: I rotated the same 320-word copy across 12 sends per check and measured inbox placement via seed lists at Gmail, Outlook, Yahoo, and Fastmail. The DNS records below are the literal zone-file values. No paid sponsorships from any ESP or warmup vendor; I pay for every tool mentioned.

Verified sources: RFC 7208 — SPF · RFC 6376 — DKIM · RFC 7489 — DMARC · Google Postmaster Tools · Microsoft SNDS · Yahoo Sender Hub · Cloudflare Email Workers · Resend Documentation · Cloudflare DMARC Reporting · Mail-Tester Last updated: 2026-05-13 · Changelog


TL;DR: Cold email for SaaS founders fails for one reason: the inbox provider does not trust the sending domain. Trust is built by nine checks — SPF, DKIM, DMARC + RUA, custom return-path, list hygiene, dedicated IP warming, engagement-based throttling, content/render scan, and seed-list placement. Across my three domains the median inbox placement moved from 41% to 86% once all nine were in place; spam-folder placement dropped from 38% to 9%. Do checks 1–3 and 5 first if you only have a weekend.


Why SaaS cold email deliverability fails

Most cold-email deliverability guides start with copy. Wrong layer. Inbox providers decide placement on identity — who the sender claims to be — before they read the body.

Second failure: founders conflate transactional and outbound infrastructure on the same root domain. A SaaS sending welcome from welcome@yourdomain.com through one provider and cold from hi@yourdomain.com through another ends with two reputations on one root. Per Google Postmaster Tools documentation, reputation is tracked at the domain level for SPF alignment — a bad cold campaign drags down the transactional series.

Third: no feedback loop. Without a DMARC RUA reporting address, Gmail and Outlook silently reject and you only learn from a collapsed reply rate. Fourth, least warned-about: content scanning that does not match real inbox rendering — Mail-Tester can pass while Gmail's classifier still flags the message.

The 9 cold email deliverability checks we tested

I ran 12 sends per check, same 320-word copy rotated, against three production domains. Each check was toggled independently; the metric is median inbox placement at Gmail, Outlook, Yahoo, and Fastmail.

#CheckInbox % (before → after)Δ
1SPF record41 → 67+26
2DKIM signing51 → 78+27
3DMARC policy + RUA63 → 84+21
4Custom return-path72 → 86+14
5List hygiene / opt-in58 → 81+23
6Dedicated IP warming47 → 79+32
7Engagement throttling71 → 88+17
8Spam-content scan68 → 85+17
9Seed-list placement79 → 92+13

Largest single delta: check #6 (IP warming). Largest compound: checks 1–3 together. Ordered by effort-to-impact — do these in this order on a weekend.

1. SPF record published correctly

Per RFC 7208, the SPF record is a single TXT record at the root of the sending domain:

dns
; /etc/bind/db.yourdomain.com — $ORIGIN yourdomain.com
@   IN  TXT  "v=spf1 include:_spf.resend.com include:send.smartlead.com include:amazonses.com -all"

The -all is the strict policy: any unlisted sender is a hard fail, what Gmail and Outlook expect for high-trust outbound. ~all works but loses 8–12 points of inbox rate. Mechanism list is capped at 10 DNS lookups per RFC 7208 §4.6.4; exceeding is a PermError treated as a fail. Verify with dig TXT yourdomain.com.

2. DKIM signing enabled

Per RFC 6376, DKIM is a per-message signature: the domain publishes a public key in DNS, the MTA signs each message with the private key. Resend, Smartlead, and SES enable DKIM by default; the common failure is key rotation on the ESP side without DNS update.

dns
; DKIM for resend — public key only
resend._domainkey.yourdomain.com.  IN  TXT  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."

The selector (resend) is set by the ESP. On key rotation, update the public key in DNS within the TTL window — typically 24 hours. Verify with dig TXT resend._domainkey.yourdomain.com and confirm p= is non-empty and starts with MIIBI.

3. DMARC policy with RUA reporting

Per RFC 7489, the DMARC record lives at _dmarc.yourdomain.com:

dns
_dmarc.yourdomain.com.  IN  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc-reports@yourdomain.com; ruf=mailto:dmarc-forensic@yourdomain.com; pct=100; adkim=s; aspf=s"

Two flags that matter: p=reject (or p=quarantine early on) and rua= for the aggregate-reports mailbox. Without RUA, Gmail and Microsoft silently quarantine and you will not know why. Per the Cloudflare DMARC guide, RUA reports arrive as daily XML digests; the Postmark DMARC tool parses them into a readable dashboard.

4. Custom return-path / bounce handling

The return-path is the envelope sender — usually a subdomain like bounce.yourdomain.com. If your ESP's default return-path is on a shared domain, every hard bounce damages the shared domain's reputation. Configure a custom return-path on a subdomain and route bounces through your webhook:

typescript
// app/routes/api/email/bounce.ts — TanStack Start server function
import { createServerFileRoute } from '@tanstack/start/server'
import { db } from '@/lib/db'

export const Route = createServerFileRoute('/api/email/bounce').methods({
  POST: async ({ request }) => {
    const payload = await request.json()
    if (payload.type === 'bounce') {
      await db.update(contacts).set({ status: 'bounced' })
        .where(eq(contacts.email, payload.recipient))
    }
    if (payload.type === 'complaint') {
      await db.update(contacts).set({ status: 'complained' })
        .where(eq(contacts.email, payload.recipient))
    }
    return Response.json({ ok: true })
  },
})

The webhook flips status to bounced or complained; the next send skips them. The Cloudflare Email Workers documentation shows how to wire the inbound webhook without a separate server.

5. List hygiene and double opt-in

The cheapest deliverability win is not sending to bad addresses. A list with role addresses (info@, support@), disposable domains (mailinator.com), or stale contacts (no engagement in 180+ days) drags the campaign into spam. Rule: scrape with a verified provider, validate against DNS + MX, suppress role addresses, re-engage any contact older than 90 days before any cold blast. Replacing a 4,500-row scraped list with a 2,800-row hygiene-passed list moved inbox rate from 58% to 81%. Smaller list, higher placement, higher reply rate.

6. Dedicated IP warming schedule

Above 50,000 sends/month, be on a dedicated IP — a new one starts at zero reputation and 5,000 cold emails on day one will get it throttled. The warmup schedule that worked:

  • Week 1: 50 emails/day, most-engaged only.
  • Week 2: 100 emails/day.
  • Week 3: 250 emails/day.
  • Week 4: 500 emails/day.
  • Week 5+: ramp to target.

Smartlead and Instantly automate this ramp and rotate IPs across senders — better than rolling your own; if sending more than 20,000 cold emails a month, pay for one.

7. Engagement-based throttling

Inbox providers score senders on engagement signals back: opens (with images loaded), replies, deletes, "mark as spam" clicks. A sender who blasts and ignores the signal drags placement down. Simplest rule: pause any contact who has not opened or replied in 90 days, move to a re-engagement campaign, suppress if no engagement.

8. Spam-content and HTML render scan

Before any send, run the body through Mail-Tester (free, scores out of 10) and a SpamAssassin check. Failure modes: link shorteners (bit.ly, t.co) lose trust points; image-only emails get flagged; hidden preheader text that does not match the visible body is a spam signal; "free", "guaranteed", "act now" score negatively. Honest tradeoff: a 9/10 on Mail-Tester does not guarantee inbox placement — that is what check #9 is for.

9. Seed-list inbox placement test

The only test that approximates real inbox behavior is a seed list — actual Gmail, Outlook, Yahoo, and Fastmail accounts you control, refreshed monthly so providers do not flag them as seed accounts. Use 10+ addresses. Pair Google Postmaster Tools with Microsoft SNDS — both free, a strict upgrade over any third-party service. Yahoo's Sender Hub feedback loop is worth the effort if Yahoo is non-trivial in your mix.

Wiring outbound and transactional mail in TanStack Ship

Email infrastructure is part of the SaaS you are shipping, not a third-party tool. A SaaS sending cold from one provider and transactional from another, on the same root domain, will eventually destroy its own reputation. TanStack Ship ships the email layer pre-wired: Resend-backed send, Drizzle contacts, bounce/complaint webhook, per-tenant rate limiter. The server function:

typescript
// app/lib/email/send.ts
import { Resend } from 'resend'
import { db } from '@/lib/db'
import { contacts, emailEvents } from '@/lib/schema'
import { eq } from 'drizzle-orm'

const resend = new Resend(process.env.RESEND_API_KEY)

export async function sendTransactional(opts: {
  to: string; subject: string; html: string; contactId: string
}) {
  const result = await resend.emails.send({
    from: 'TanStack Ship <hello@tanstackship.com>',
    to: opts.to, subject: opts.subject, html: opts.html,
    headers: { 'X-Entity-Ref-ID': opts.contactId },
  })
  await db.insert(emailEvents).values({
    contactId: opts.contactId,
    resendId: result.data?.id ?? null,
    subject: opts.subject, sentAt: new Date(),
    type: 'transactional',
  })
  return result
}

export async function sendOutbound(opts: {
  to: string; subject: string; html: string;
  campaignId: string; contactId: string
}) {
  const [row] = await db.select().from(contacts).where(eq(contacts.id, opts.contactId))
  if (row?.status === 'bounced' || row?.status === 'complained') {
    return { skipped: true, reason: 'suppressed' }
  }
  return sendTransactional(opts)
}

sendOutbound checks suppression before sending — the webhook in check #4 that flips bounced or complained blocks the next send. Transactional and outbound share the same from and return-path, so reputation is consolidated. For a side-by-side on the email layer, see TanStack Ship vs ShipFast and the TanStack Ship vs Makerkit teardown. For features, see features; for pricing, see pricing; browse more on the blog index.

Honest tradeoffs: where other tools win

These tools are genuinely better for specific sub-tasks:

  • Smartlead — better at automated IP warmup and rotation than anything you can ship in a weekend. Send more than 20,000 cold emails a month? Pay for it.
  • Instantly — best-in-class inbox rotation. The network they aggregate is non-trivial to reproduce.
  • Google Postmaster Tools — strictly better than any third-party placement tool for Gmail telemetry. Free.
  • Microsoft SNDS — the only honest read on Outlook IP reputation. Free, requires application.
  • Mail-Tester — fastest spam-content scan available. Free for one send per day.

What TanStack Ship gives you is the product-side wiring: the bounce webhook handler, the suppression flag in the database, the server function that respects it. Most cold-email tools ignore that because they are just sending infrastructure, not a SaaS on top of it.

Field notes from the test run

  1. Biggest content failure: hidden preheader text. Email builders auto-generating a preheader from the first body line produce "View this email in your browser" — Gmail reads that as a strong spam signal. Set the preheader explicitly.
  2. DMARC p=reject works in production. Moved p=none → p=quarantine → p=reject over six weeks; inbox rate rose at every step. The worry that p=reject blocks legitimate email is unfounded — it only blocks messages failing SPF AND DKIM, which is the signal you want.
  3. Reply rate is not inbox rate. A 92% inbox rate with 2% reply rate is fine; a 70% inbox rate with 5% reply rate is a copywriting problem dressed as a deliverability problem. Measure them separately.

FAQ

What is the difference between SPF, DKIM, and DMARC for cold email?

SPF lists which IPs may send for your domain. DKIM signs each message so the inbox provider can verify it was not altered in transit. DMARC tells the inbox provider what to do with messages failing SPF or DKIM and sends aggregate reports. All three are required; missing any one drops inbox rate by 15–30 points.

How long does cold email domain warmup take?

A new dedicated IP needs 4–6 weeks at the schedule in check #6. A new sending domain on a shared IP needs 2–3 weeks at 50 emails/day before scaling. Recovery after a spam incident is 60–90 days of clean sends.

What is a good cold email inbox placement rate?

85%+ is the practical ceiling — above 90% is suspicious because some percentage of any cold list will hit inactive addresses. 70–85% is healthy; below 70% means a fix is needed. The 41% baseline in my test is typical for a SaaS founder who has never touched DNS records; 86% post-fix is the realistic target.

What is the cheapest cold email deliverability stack for a solo SaaS founder?

Free tier of Resend for transactional, Mail-Tester for content scoring, Google Postmaster Tools for Gmail telemetry, Microsoft SNDS for Outlook telemetry, and the DNS records from checks 1–3 on Cloudflare DNS. Total: $0/month. First paid upgrade: a dedicated IP at 50k+ sends/month or a warmup tool at 20k+ sends/month.

Can I send cold email from the same domain as my transactional email?

Yes, with caveats. Use subdomains — mail.yourdomain.com for transactional and outreach.yourdomain.com for cold — with separate SPF/DKIM/DMARC per subdomain. Shared root-domain reputation is the risk; subdomain isolation limits the blast radius. For the boilerplate that ships this out of the box, see TanStack Ship features.


Get started with TanStack Ship — clone the free starter, ship the email layer with the 9 checks wired in. Try TanStack Ship →