Bounces and suppression

You need: email composed and provisioned, with the bounce routing rule created.

Four reasons an address is on the list

Deliverability is how to keep addresses off it, tracking is where the complaints come from, the job model is what a suppressed send becomes, and receiving mail is how a bounce reaches you at all.

ReasonBlocks
hard_bounceEverything
complaintEverything
unsubscribeElective mail only
manualDepends what you set it for

That asymmetry is the whole point. Somebody who unsubscribed from your newsletter has not stopped being able to sign in — so a magic link still reaches them.

The kind comes from the template, never from the call site, which is what makes that hold.

Four bounce classifications

hardPermanent. Suppress
softTransient. Cloudflare auto-retries
complaintReported as spam. Suppress
auto_replyA vacation responder. A no-op, never suppressed

The last one earns its slot. An out-of-office is not a delivery failure, and a system that suppressed on one would quietly stop mailing everybody who went on holiday.

The suppression database is shared across environments

Deliberately, and it is the opposite of every other capability’s posture.

An unsubscribe has to apply everywhere. Somebody who opted out in production has not agreed to receive staging’s mail either.

Compare: storage and media keep a bucket per environment precisely so staging cannot write into production’s. Two answers to one question, and each is about what the data means.

Suppression is consulted twice

At send — the authority, because whether an address is blocked is a question about the instant of sending.

At enqueue — so the caller learns. A blocked recipient never becomes a queued send: the row is born suppressed and the reason comes back on the result.

Without that, a three-person account whose addresses have all hard-bounced is three ordinary skips in a send log nobody reads.

Managing the list

Scope
GET /email/suppressionsemail:suppressions:read
POST /email/suppressionsemail:suppressions:write
POST /email/suppressions/removeemail:suppressions:delete

The bounce routing rule

One Email Routing rule points bounce and complaint mail at the production app Worker — one per domain.

It is opt-in and explicit, because enabling Email Routing on a zone points its MX at Cloudflare and would move your real inbound mail.

The rule is named <project>-global-email-bounce, distinct from the support capability’s inbound rule — idempotency keys on the rule name, and sharing one would make whichever capability provisioned second believe its rule already existed.

One inbound Worker per domain, and what that costs you

Bounce attribution is platform-limited.

A production campaign’s full funnel lands in production’s database. Feature and staging environments see their own opens, clicks and unsubscribes — but not asynchronously-routed bounces, because there is one inbound Worker for the domain and it is production’s.

That is a platform constraint stated rather than discovered.

email/suppressed is not a failure to fix

The address is on the opt-out list, and that is the list working.

ESC