Sending one email is easy. Getting millions of them into inboxes, reliably and on time, is a distributed system with an unusual property: the other side is thousands of independent receivers, each running its own filters and each deciding whether to trust you based on your past behaviour. Most delivery failures are not code bugs. They are a missing DNS record, a sender domain that does not align, a retry storm against a provider that asked you to slow down, or a reputation damaged by mailing addresses that bounced last month.
This article builds the system from first principles: how a message travels over SMTP and what the receiver checks at each hop, the authentication trio of SPF, DKIM and DMARC and the alignment rule that ties them together, transport security, the queueing and retry design of the sending side, bounces and complaints, reputation and the bulk-sender rules large mailbox providers now enforce, and a worked setup for an online store. It ends with the failures you will meet and a checklist.
The path of a message
Your application hands a message to a sending service, over an HTTP API or by authenticated SMTP submission on port 587. The sending side stores, renders and signs it and puts it in a queue. An outbound mail transfer agent (MTA) looks up the recipient domain's MX records, connects to one of those hosts on port 25 and runs an SMTP transaction. The receiving MX accepts or rejects the message, filters it, and files it in the inbox or spam. Later problems come back as bounces or complaints.
A message has two senders. The envelope sender, given in MAIL FROM, receives bounces and is invisible to the reader. The header From is what the reader sees. They often differ on purpose, because the envelope sender encodes a per-message bounce address. Authentication proves the right to use these domains; DMARC ties the one the reader sees to the ones that were proven.
An SMTP transaction
SMTP is a line-based conversation. The client introduces itself with EHLO, upgrades to TLS with STARTTLS if offered, gives the envelope sender and recipients, then sends the message itself after DATA, ending with a line containing only a dot. Each server reply starts with a three-digit code: 2xx means success, 4xx means a temporary failure that you should retry later, and 5xx means a permanent failure that you must not retry. Enhanced status codes such as 5.1.1 (unknown mailbox) or 4.7.x (policy, often rate limiting) give the reason.
S: 220 mx.example.net ESMTP
C: EHLO mta1.mail.shop.example
S: 250-mx.example.net
S: 250-STARTTLS
S: 250 SIZE 52428800
C: STARTTLS
S: 220 2.0.0 Ready to start TLS
... TLS handshake, then EHLO again ...
C: MAIL FROM:<b+8f3a2c@mail.shop.example> <- envelope sender (bounces go here)
S: 250 2.1.0 OK
C: RCPT TO:<ana@example.net>
S: 250 2.1.5 OK
C: DATA
S: 354 Go ahead
C: From: Shop <orders@shop.example> <- header From: what the user sees
C: Subject: Your order A-1042
C: DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=shop.example; s=s2026a;
C: h=from:to:subject:date:message-id; bh=...; b=...
C: ...
C: .
S: 250 2.0.0 OK queued
C: QUITNote that a 250 after the dot means the receiving server accepted responsibility for the message, not that it reached the inbox. Spam placement happens after acceptance and is invisible to you in SMTP; you only see it through engagement data and provider dashboards.
SPF, DKIM and DMARC
SPF lets a domain publish which IP addresses may send mail using it as the envelope sender. The receiver takes the MAIL FROM domain, fetches its SPF TXT record and checks the connecting IP against it. SPF evaluation is limited to 10 DNS lookups, counting include, a, mx and similar mechanisms, and exceeding it is an error that fails the check; chaining several vendors' includes on one domain is the usual way people cross it. SPF also breaks on forwarding, because the forwarder's IP is not in your record.
DKIM signs the message. The sender signs a hash of the body and selected headers with a private key; the DKIM-Signature header names the signing domain d= and selector s=, and the receiver fetches the public key from selector._domainkey.domain. DKIM survives forwarding if the signed parts are unmodified. Use 2048-bit RSA keys and rotate by publishing a new selector, switching to it, and removing the old key once in-flight mail has drained.
DMARC is published at _dmarc under the header From domain. It passes when SPF or DKIM passes and the domain that passed aligns with the From domain. Relaxed alignment, the default, accepts any domain under the same organisational domain, so mail.shop.example aligns with shop.example; strict alignment requires an exact match. The policy p=none, quarantine or reject tells receivers what to do with failures, and rua asks them to send daily aggregate reports, which is how you discover the forgotten system that sends mail as your domain.
; SPF for the envelope (MAIL FROM) domain: only these hosts may send for it
mail.shop.example. TXT "v=spf1 ip4:203.0.113.16/28 -all"
; DKIM public key, looked up as <selector>._domainkey.<d=>
s2026a._domainkey.shop.example. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqh..."
; DMARC policy for the From: domain, with aggregate reports
_dmarc.shop.example. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@shop.example; adkim=r; aspf=r"
; inbound transport security for mail sent TO shop.example
_mta-sts.shop.example. TXT "v=STSv1; id=20260930"
_smtp._tls.shop.example. TXT "v=TLSRPTv1; rua=mailto:tls-reports@shop.example"Mailing lists and forwarders that modify messages break both SPF and DKIM. ARC, the Authenticated Received Chain, lets each intermediary record the authentication results it saw and sign them, so a final receiver that trusts the intermediary can still accept the message.
Transport security
SMTP between servers uses opportunistic TLS: STARTTLS is used if the receiver advertises it, but an attacker who strips the advertisement can force plain text. MTA-STS closes that gap for mail sent to your domain. You publish a TXT record at _mta-sts and a policy file over HTTPS listing your MX hosts and a mode; senders that support MTA-STS then refuse to deliver without valid TLS to those hosts. TLS-RPT, published at _smtp._tls, asks senders to report TLS failures to you. Start MTA-STS in testing mode, read the reports for a few weeks, then switch to enforce. On the sending side, always offer TLS; large providers require it.
The sending system: queues, throttles and retries
The sending side is a queueing system whose job is to be a good citizen towards each receiver. The accept path is synchronous and cheap: validate, check the suppression list, store the message durably with an idempotency key so a client retry does not send twice, and return an ID. Everything after that is asynchronous.
Queue by destination provider, the unit receivers rate-limit you by. Each destination gets its own concurrency limit and rate, so a throttling provider backs up only its own queue. When a receiver answers with a 4xx policy code, cut that destination's concurrency and back off; hammering it is how a temporary deferral becomes a block.
Retries use exponential backoff with jitter. RFC 5321 suggests waiting at least 30 minutes between attempts once the first retry has failed and giving up after four to five days; most systems also try a quick early retry, because many deferrals are greylisting that clears within minutes. After the give-up time the message becomes a bounce.
BACKOFF = [5 * 60, 30 * 60, 60 * 60, 2 * 3600, 4 * 3600, 8 * 3600] # seconds
GIVE_UP_AFTER = 4 * 24 * 3600 # RFC 5321: 4-5 days
def handle_reply(msg, reply, now):
code = reply.code # e.g. 250, 421, 450, 550
if 200 <= code < 300:
mark_delivered(msg)
elif 500 <= code < 600: # permanent: do not retry
record_bounce(msg, reply, hard=is_mailbox_failure(reply))
if is_mailbox_failure(reply): # 5.1.1 unknown user, 5.1.2 bad destination domain
suppress(msg.rcpt)
else: # 4xx or connection failure: temporary
if now - msg.first_attempt > GIVE_UP_AFTER:
record_bounce(msg, reply, hard=False)
return
if reply.enhanced.startswith("4.7"): # policy / rate deferral
slow_down(msg.dest_domain) # cut this domain's concurrency
delay = BACKOFF[min(msg.attempts, len(BACKOFF) - 1)]
schedule(msg, now + delay * random.uniform(0.8, 1.2)) # jitterThe same patterns appear in any delivery pipeline; see message queues, idempotency, distributed rate limiting, dead-letter queues and, for fan-out across email, push and SMS, notification system architecture.
Bounces, complaints and unsubscribes
Some failures arrive during the SMTP session as 5xx replies. Others arrive later, when a server that accepted the message fails to deliver it and sends a delivery status notification back to the envelope sender. That is why the envelope sender encodes the message: with a per-message address such as b+8f3a2c@mail.shop.example, the bounce handler knows exactly which message and recipient failed without parsing free-form text.
Classify every failure. Hard bounces, such as unknown user or non-existent domain, go straight onto the suppression list; mailing them again damages your reputation. Soft bounces, such as a full mailbox, are retried and suppressed after repeated failures over several days. Complaints, when a user marks a message as spam, come back through provider feedback loops or aggregate dashboards, and the address should be suppressed from marketing mail at once.
Marketing and subscribed mail should carry one-click unsubscribe as defined in RFC 8058: a List-Unsubscribe header with an HTTPS URL and a List-Unsubscribe-Post header, both covered by the DKIM signature. The mailbox provider then shows its own unsubscribe button and POSTs to your URL, which must work without a login or a confirmation page.
List-Unsubscribe: <https://shop.example/u/9c1e7d>, <mailto:unsub+9c1e7d@shop.example>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Reputation and bulk-sender rules
Receivers score both your sending IPs and your domains. Domain reputation matters more today because IPs are shared and change, while the DKIM domain follows you. Reputation is built by volume that recipients want: low bounce rates, low complaint rates, and people who open and reply. A new IP or domain has no history, so warm it up by starting with a small daily volume to your most engaged recipients and increasing it over several weeks while watching deferrals.
Separate streams: send transactional mail from a different subdomain, ideally on different IPs, than marketing mail, so a bad campaign cannot delay password resets. Google's sender guidelines now make much of this mandatory for anyone sending more than 5,000 messages a day to Gmail personal accounts: SPF, DKIM and DMARC must all be set up, the From domain must align with the SPF or DKIM domain, marketing mail must support one-click unsubscribe, the spam rate reported in Postmaster Tools must stay below 0.30% with a target below 0.10%, sending IPs must have matching forward and reverse DNS, and connections must use TLS. Other large providers publish similar rules; check each one you depend on.
Worked example: a store sending receipts and a newsletter
An online store sends about 200,000 receipts and shipping notices a day and a weekly newsletter to 1.5 million subscribers. It uses shop.example as the visible From domain for both, but separates the streams underneath: transactional mail uses envelope and DKIM domain tx.shop.example on one pool of IPs, and the newsletter uses news.shop.example on another. Both align with shop.example under relaxed DMARC.
DNS gets an SPF record on each subdomain listing only its own pool, a DKIM selector per stream, and a DMARC record at p=none with aggregate reports. After two weeks of reports show that every legitimate source passes, including a helpdesk tool that was sending unsigned mail and had to be given its own DKIM key, the policy moves to quarantine and later to reject.
The newsletter IPs are new, so the first sends go to subscribers who opened in the last 30 days, roughly doubling volume every few days. Addresses unengaged for a year get a re-permission campaign and are removed if silent. The queue caps concurrency per large provider and halves it on any 4.7 deferral, and receipts never wait behind the newsletter.
Failure modes
| Symptom | Likely cause | First response |
|---|---|---|
| DMARC failures in reports for your own mail | Vendor sends as your domain without alignment | Give the vendor a DKIM key on your domain or a subdomain |
| SPF permerror | More than 10 DNS lookups from chained includes | Move vendors to subdomains; remove unused includes |
| Mail accepted (250) but lands in spam | Reputation, complaints or poor engagement | Check provider dashboards; stop mailing unengaged users |
| Growing 4.7.x deferrals at one provider | Rate or reputation throttling | Cut concurrency to that provider; do not retry harder |
| Password resets delayed during campaigns | Shared queue and IPs for all streams | Separate transactional and marketing streams |
| Bounce rate spike after an import | Old or purchased addresses | Suppress hard bounces; verify list source |
| Forwarded mail rejected | SPF breaks on forwarding, DKIM altered | Rely on DKIM alignment; receivers may use ARC |
What to do next
- Inventory every system that sends mail as your domain, then publish DMARC at
p=nonewithruareports and read them. - Give each stream its own subdomain with a narrow SPF record and its own DKIM selector, and check alignment with the From domain.
- Move DMARC to quarantine and then reject once every legitimate source passes.
- Queue by destination provider with per-provider concurrency, backoff with jitter on 4xx, and a four-to-five-day give-up time.
- Use per-message envelope senders, classify bounces, and suppress hard bounces and complainers automatically.
- Add RFC 8058 one-click unsubscribe to marketing mail, watch spam rates in provider dashboards, and warm new IPs and domains gradually.