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.

Advertisement

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.

The path of one message, and the checks applied at each hopYour appsend API callMessage storeid, render, signPer-domain queuesthrottle, retryOutbound MTADKIM sign, TLSDNSMX, MTA-STSlookupReceiving MXport 25, SMTPSMTP over TLSFiltersSPF, DKIM, DMARCInbox or spamreputation, contentBounce handlerDSN, suppression5xx / DSNComplaintsFBL, unsubscribesuser marks spamsuppressA 4xx reply keeps the message in the per-domain queue for a later retry; a 5xx reply or a later bounce message goes to the bounce handler.The receiver decides inbox or spam from authentication, sender reputation and engagement, not from content alone.
One message from the application to the inbox. Queues are per destination domain; 4xx replies loop back into the queue, 5xx replies and later bounce messages feed suppression, and complaints come back from the receiver.

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: QUIT

Note 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.

Advertisement

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))   # jitter

The 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

SymptomLikely causeFirst response
DMARC failures in reports for your own mailVendor sends as your domain without alignmentGive the vendor a DKIM key on your domain or a subdomain
SPF permerrorMore than 10 DNS lookups from chained includesMove vendors to subdomains; remove unused includes
Mail accepted (250) but lands in spamReputation, complaints or poor engagementCheck provider dashboards; stop mailing unengaged users
Growing 4.7.x deferrals at one providerRate or reputation throttlingCut concurrency to that provider; do not retry harder
Password resets delayed during campaignsShared queue and IPs for all streamsSeparate transactional and marketing streams
Bounce rate spike after an importOld or purchased addressesSuppress hard bounces; verify list source
Forwarded mail rejectedSPF breaks on forwarding, DKIM alteredRely on DKIM alignment; receivers may use ARC

What to do next

  1. Inventory every system that sends mail as your domain, then publish DMARC at p=none with rua reports and read them.
  2. Give each stream its own subdomain with a narrow SPF record and its own DKIM selector, and check alignment with the From domain.
  3. Move DMARC to quarantine and then reject once every legitimate source passes.
  4. Queue by destination provider with per-provider concurrency, backoff with jitter on 4xx, and a four-to-five-day give-up time.
  5. Use per-message envelope senders, classify bounces, and suppress hard bounces and complainers automatically.
  6. Add RFC 8058 one-click unsubscribe to marketing mail, watch spam rates in provider dashboards, and warm new IPs and domains gradually.
Key takeaway: Email delivery is a queueing system talking to thousands of independent receivers that decide, message by message, whether to trust you. Trust starts with SPF, DKIM and DMARC aligned to the From domain and TLS on every hop, and is kept by behaviour: per-provider throttling that backs off on deferrals, strict bounce and complaint suppression, one-click unsubscribe, separate streams for transactional and marketing mail, and gradual warm-up. A 250 reply only means accepted; inbox placement is earned through reputation.