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.
| Reason | Blocks |
|---|---|
hard_bounce | Everything |
complaint | Everything |
unsubscribe | Elective mail only |
manual | Depends 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
hard | Permanent. Suppress |
soft | Transient. Cloudflare auto-retries |
complaint | Reported as spam. Suppress |
auto_reply | A 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/suppressions | email:suppressions:read |
POST /email/suppressions | email:suppressions:write |
POST /email/suppressions/remove | email: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.