LetterDuck
Field guide

The email deliverability handbook

Our production deliverability playbook, published. The compliance floor, the warm-up ramp, the complaint math, and the monitoring stack, with the exact thresholds our send path enforces.

Two of the three biggest mailbox providers now refuse unauthenticated mail outright. Microsoft started returning "550 5.7.515 Access denied" to non-compliant high-volume senders in May 2025, and Gmail moved from spam-foldering to outright rejection in November 2025. The penalty for getting deliverability wrong is no longer the junk folder. It is a bounce.

This handbook is our internal playbook made public. We run LetterDuck, an email and newsletter workspace that has been live on our own domain since June 2026, and every threshold below is enforced in our send path as code, not as a guideline someone is supposed to remember. Where we describe an incident, it happened to us. Where we quote a number, it carries a source and a date, or it is our own measurement and we say so.

What email deliverability actually is

Delivery means the receiving server accepted your message. Deliverability means a human can actually find it. A "250 OK" from Gmail's servers tells you nothing about whether the message landed in the inbox, the Promotions tab, or the spam folder, and no provider will report the difference to you directly. You infer it from complaint dashboards, engagement, and the occasional angry reply that starts with "found this in spam."

Reputation attaches to two things: the sending IP and, increasingly, the sending domain. On a shared relay pool the IP is already established, so in practice your domain's history is what you are building, and it follows you across providers.

One number gets misread constantly. Gmail defines a "bulk sender" as anyone sending 5,000 or more messages a day to Gmail addresses. That is a reporting threshold, not a permission slip. Authentication is checked on every message and complaint math works at any volume. We follow the full ruleset at dozens of sends a day, and this handbook assumes you will too.

This page is the reference. If your mail is already disappearing and you want a diagnostic checklist instead, start with why emails go to spam and come back.

The compliance floor: SPF, DKIM, and DMARC, aligned

Three DNS-published standards decide whether your mail even gets considered. SPF is a TXT record listing which servers may send mail for your domain. DKIM is a cryptographic signature on each message, verified against a public key in your DNS. DMARC ties the two to the From address the recipient sees and tells receiving servers what to do when a message fails.

The word that trips people up is alignment. SPF can pass for a relay's bounce domain while your visible From address is somewhere else entirely, and DMARC will still fail the message. At least one of SPF or DKIM must pass for the same domain that appears in the From header. Every commercial relay documents how to achieve this; the point of checking is that "SPF passes" and "SPF passes aligned" are different results.

The records themselves are short. A minimal, sane starting set looks like this:

TXT  yourdomain.com           "v=spf1 include:_spf.yourrelay.com ~all"
TXT  s1._domainkey.yourdomain.com   (the DKIM public key your relay generates for you)
TXT  _dmarc.yourdomain.com    "v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com"

Treat DMARC as a progression, not a checkbox. Publish p=none first, which changes nothing about delivery but starts sending you aggregate reports. Read them for a couple of weeks to find every legitimate service sending as your domain. Move to p=quarantine, then to p=reject once the reports come back clean. Jumping straight to p=reject is how people discover their invoicing tool was sending as their domain, three days after its mail stops arriving.

We keep the deep dives separate: how SPF, DKIM, and DMARC interlock, then a full walkthrough each for writing an SPF record, how DKIM signing works, and DMARC policies and reports. If you want the one-page orientation first, read our email authentication overview.

Two more items round out the floor. Your sending IP needs a PTR record (reverse DNS), which a relay's shared pool provides, and mail must move over TLS, which any modern relay negotiates for you. These records all live in the same DNS zone as the MX records that route your inbound mail; if you are standing up a domain from scratch, our guide to DNS records for email covers the receiving side.

February 2024 to November 2025: from junk folder to hard reject

The rules changed in three dated steps, and each one raised the price of non-compliance.

Google and Yahoo's bulk sender requirements took effect in February 2024: SPF and DKIM aligned, DMARC published, one-click unsubscribe, and a spam complaint rate held under the thresholds covered below. Enforcement was soft at first. Non-compliant mail got filtered, deferred, or quietly down-ranked.

Microsoft joined in May 2025. Senders of 5,000+ messages a day to outlook.com, hotmail.com, and live.com addresses without aligned SPF, DKIM, and DMARC now get a permanent rejection: "550 5.7.515 Access denied, sending domain does not meet the required authentication level" (Microsoft's Defender for Office 365 team announced this in April 2025).

Gmail closed the loop in November 2025, ending its soft-enforcement period. Messages that fail the requirements are now rejected during the SMTP conversation with permanent 5xx errors, each with a code that names the failure: 5.7.27 for SPF, for example (Google's sender guidelines; Proofpoint covered the rollout in October 2025). A rejected message never reaches a spam folder where a curious recipient might rescue it. It just bounces.

We keep an annotated copy of the full ruleset, requirement by requirement, in Gmail and Yahoo's bulk sender requirements. Read it once. Most of it is this handbook in list form.

RFC 8058 one-click unsubscribe is a pair of message headers that let a mailbox provider offer an unsubscribe button in its own UI. Both are required, and this is exactly what we stamp on every campaign send:

List-Unsubscribe: <https://yourdomain.com/u/TOKEN>, <mailto:unsub@yourdomain.com?subject=TOKEN>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The rules give you two days to honor the request. Honor it immediately; there is no engineering reason a database write should take two days, and every message sent to someone who already asked out is a complaint risk. Keep the footer link too, since the headers and the visible link serve different readers.

The logic here is the least intuitive part of deliverability, so it is worth stating plainly: an easy exit protects your reputation. A subscriber who cannot find the unsubscribe link does not give up. They press the spam button instead, and a complaint counts against the 0.1% rate that decides your delivery while an unsubscribe costs you nothing but a name. Our full write-up on the List-Unsubscribe header covers the token design and what Gmail actually does with the POST request.

Spam complaint rate: 0.1% is the target, 0.3% is the cliff

The spam complaint rate is the share of your delivered mail that recipients mark as spam, as measured by the mailbox provider. Google's published thresholds since February 2024: stay under 0.1%, and never reach 0.3%. In absolute terms, 0.3% is three spam clicks per thousand delivered messages. A small list can cross it with a handful of annoyed people.

The cliff is what makes 0.3% different in kind, not just degree. Once you touch it, Google's guidelines say delivery mitigation is withheld until your rate has stayed back under 0.3% for seven consecutive days. There is no support ticket that shortcuts the seven days. You serve them.

Gmail also offers no traditional feedback loop that reports individual complainers back to you, so you cannot simply suppress the people who clicked. The mechanism that exists instead is the Feedback-ID header, which lets Postmaster Tools attribute spam rates to your campaigns; we stamp it on every send. Our deliverability panel plots the measured spam rate against the 0.1% and 0.3% lines, and the send path pauses itself at the cliff rather than waiting for a human to notice. The full arithmetic, including why one complaint means different things at different list sizes, is in spam complaint rate, explained.

Bounce rate: under 2%, with different rules for hard and soft

A hard bounce is a permanent failure: the address does not exist, the domain has no mail server. A soft bounce is temporary: mailbox full, server briefly unavailable. They demand opposite responses, which is why treating "bounce" as one category corrupts your list handling.

Our policy, enforced automatically: hard bounces are suppressed permanently on the first occurrence. Soft bounces retry about three times over 14 days, then convert to suppressed. Everything lands in one suppression list per domain that also collects complaints and unsubscribes, and that list is checked before every single send. Not before every campaign. Before every message.

We hold the overall bounce rate under 2%. For a sense of how seriously infrastructure providers take this, AWS SES will pause an entire account at a 5% bounce rate or a 0.1% complaint rate. We built auto-suppression before we were anywhere near SES's scale, because retrofitting hygiene onto a damaged list is far harder than never damaging it. The distinction between bounce types, with the SMTP codes behind each, is in hard bounce vs soft bounce.

Warm-up: the ramp we enforce in code

A new domain has no sending history, and mailbox providers treat no history as suspicion. Reputation matures over roughly 4 to 8 weeks of consistent sending. Sudden volume from a fresh domain is the single most spammer-like pattern there is, so warm-up means starting small and growing on a schedule.

On a shared-pool relay you are not warming an IP; the pool's IPs are already established and the relay manages them. You are warming your domain. That changes nothing about the discipline and everything about the mechanics: no ESP switch resets your domain history, for better or worse.

This is the ramp curve our send path computes. It enforces the lower of this and the relay ceiling, which on our current plan is 95 sends a day, so in practice the ceiling is what binds us. It codifies Resend's documented domain ramp for days 1 through 7, then the growth gate we apply after:

PhaseDaysSend toDaily capIncrease
Warm1-7Most-engaged segment (opened or clicked within 90 days)~150 growing to ~1,129x1.4 per day
Ramp8-28Engaged first, then the full list+10-20% per daytarget: complaints < 0.1%, bounces < 2%
Steady29+Full list, minus sunset inactivesrelay caphold

The gate in the middle row is the part most warm-up advice leaves out. Volume growth is conditional: if complaints reach 0.1% or bounces reach 2%, growth stops until the numbers recover. The send path auto-pauses entirely at a 0.3% complaint rate or a bounce rate above 2%. No human judgment call sits in that loop. Consistency matters as much as the ceiling; a gap of several days followed by a burst reads worse than steady modest volume, so never raise volume more than 30% in a day even when things look great. 10-20% is the comfortable range.

One related decision people reach too early: the dedicated IP. Below about 50,000 emails a month you cannot send enough to keep a dedicated IP warm, and an underfed IP has worse deliverability than a well-run shared pool. Postmark suggests roughly 300,000 a month before a dedicated IP pays off; SES's guidance implies about 100,000 a day. Stay on the shared pool until the math clearly favors leaving. Day-by-day instructions, including what to do when a ramp stalls, are in our email warm-up guide.

Stream separation: bulk mail on its own subdomain

Send your newsletter from a marketing subdomain like news.yourdomain.com, and keep transactional mail and 1:1 replies on the root domain. The reason is blast-radius control: newsletters are where complaints happen, and a rough campaign should never be able to drag down password resets or a reply to a customer.

Each subdomain gets its own aligned SPF and DKIM, and once a stream is warm you can publish DMARC p=reject for it independently. The subdomain inherits some trust from the organizational domain, but builds its own record from there.

The honest caveat, which subdomain advice usually omits: a subdomain is isolation, not a wall. Mailbox providers roll sustained abuse up to the parent domain, so a subdomain buys you insulation from a bad week, not a place to misbehave. And a matching disclosure about our own practice: at our current scale (small lists, dozens of sends a day) we still send newsletters from the root domain, and we will split streams as volume grows. Separation earns its complexity somewhere past a few thousand messages a month, not at message one.

Engagement: replies outrank clicks, clicks outrank opens

Mailbox providers score how recipients treat your mail, and the signals are not equal. A reply is the strongest positive signal there is. A click is next. An open is a distant third, and getting weaker: since Apple shipped Mail Privacy Protection with iOS 15 in September 2021, Apple Mail preloads images through a proxy, so an "open" fires whether or not a human ever looked. Score your list on clicks and replies. Never make a keep-or-cut decision on opens.

Two provider quirks are worth knowing. Gmail weights recency: a subscriber who engaged last week helps you more than one who engaged in March. Outlook weights consistent history: it likes senders who have behaved for a long time, which is another argument for the steady cadence in the warm-up section.

The practical moves follow from the hierarchy. Send from an address that accepts replies, because no-reply@ throws away your best signal and quietly tells recipients you are not listening. Ask a real question occasionally. And when you do reply to someone, thread properly: outbound replies should carry In-Reply-To and References headers so the conversation stays intact in the recipient's client. Our send path sets both on every reply, from any address on the domain.

List hygiene: who you send to matters more than what you send

Most deliverability damage is self-inflicted at signup or by inertia afterward. The controls are unglamorous and they work.

Use double opt-in: a signup is not a subscriber until they click the confirmation link. This single control filters typos, bots, and spite signups, and it is the main defense against pristine spam traps, addresses that exist only to catch senders who mail without consent.

Reject role accounts at signup: info@, admin@, postmaster@, abuse@. These are shared mailboxes read by nobody in particular and by complaint-happy someones in the worst case. Our signup validation refuses them outright.

Sunset inactives on a 60-to-90-day clock. A subscriber who has not clicked in 90 days is a reputation liability wearing a list-size trophy. Try one re-engagement message, then stop sending. Recycled spam traps are dead mailboxes that providers resurrect specifically to catch senders who never prune; a sunset policy makes them a non-issue.

And never buy a list. Purchased addresses combine every failure mode above: no consent, high complaint rates, embedded traps, and bounce rates that will trip the 2% gate in a single send.

Monitoring: a $0 stack that covers you

Every number in this section costs nothing to obtain, and none of it shows up unless you enroll.

Google Postmaster Tools is the one that matters most. Note the 2025 change: Google began redirecting everyone to Postmaster Tools v2 on September 30, 2025, retired the old domain and IP reputation dashboards outright, and shut down the v1 API at the end of 2025. The spam-rate chart is now the primary signal, which is fitting, since spam rate is the number with published thresholds. The v2 API is free, OAuth-based, and automatable across domains; setup and what each remaining dashboard means are in our Google Postmaster Tools guide.

Microsoft SNDS gives you IP-level data for Outlook properties, and JMRP sends actual complaint reports back. Yahoo's complaint feedback loop enrolls by the DKIM signing domain, the d= value rather than the selector, and covers AOL as well. Enroll in all of them the week you start sending; they cost nothing and take minutes each.

Before every campaign, run the draft through mail-tester.com. It scores authentication, content, and list headers out of 10 and names each deduction. We treat it as a pre-send gate, not a curiosity. Separately, check whether your domain or relay IP has landed on a blocklist; in practice only a few lists (Spamhaus above all) affect real delivery, and how to check email blacklists walks through which ones matter and how removal works.

Monitor the inbound path too, because failures there are silent. Our own worst incident proves the point. In June 2026 we were mirroring inbound mail to a Gmail mailbox using Cloudflare's message.forward(), and Gmail was rejecting about 73% of it, because forwarding re-sends a message from infrastructure the original author's DMARC policy never authorized. Nothing errored on our side. The mail just never arrived. We found it by noticing absences, which is the worst possible monitoring system, then replaced forwarding with an API insert and kept SMTP forwarding only as a last-resort fallback. If you take one lesson from our logs: forwarding breaks DMARC, and DMARC failures at the receiving end are invisible to the sender.

That incident is why a canary now runs in production. Every two hours, a probe message travels our entire real inbound path: DNS, the catch-all address, Cloudflare's email routing, the Worker, the parser, the database. It is absorbed on ingest and never shown to a user. If it fails to round-trip, the owner gets an email within the hour. Silence means healthy. You can build the same thing with a cron job and one test address, and we would argue you should.

Content hygiene: the low-order bits

Content is the least important layer, which is itself worth knowing: authentication and reputation dominate modern filtering, and no subject-line thesaurus rescues a domain with a 0.4% complaint rate. But content mistakes still cost points at the margin, and they are cheap to avoid.

Send multipart/alternative with a real plain-text part, not an empty one. Keep the text-to-image ratio around 60:40 or more text, and never send image-only mail. Do not use link shorteners; link with your sending domain so the click target matches the sender. Keep the From name and address stable from send to send, since recognition is a filter input and an inconsistent identity resets it. Put alt text on images. Skip the fake "Re:" and "Fwd:" subject prefixes and the all-caps urgency; filters have seen forty years of both.

Our newsletter template enforces all of this at render time, which we recommend over remembering it at 11 p.m. before a send.

Where to start

Deliverability work compounds, so order matters. This is the sequence we would run on any new domain:

  1. Publish SPF, DKIM, and DMARC at p=none today. Verify alignment with a test send, not by squinting at DNS.
  2. Add the two RFC 8058 headers to every bulk template and wire the unsubscribe endpoint to act immediately.
  3. Connect bounce and complaint webhooks to a suppression list that is checked before every message goes out.
  4. Warm the domain on the table above: 150 sends to your most engaged people on day 1, x1.4 daily, gate all growth on complaints under 0.1% and bounces under 2%.
  5. Register for Postmaster Tools v2, SNDS, JMRP, and Yahoo's feedback loop. Run mail-tester before each campaign.
  6. Put clicks and replies on a dashboard, ignore opens, and sunset anyone quiet for 90 days.

Steps 1 through 3 take an afternoon. Reputation takes 4 to 8 weeks. Start with the afternoon.