Suppression-List Architecture for Sending Platforms
How a multi-tenant platform scopes suppression — global vs account vs tenant vs stream — with auto-suppression triggers, precedence and override semantics, retention, and bulk transfer, extracted from AWS SES's documented design.
How a platform that sends for many customers should scope, populate, and override do-not-send lists. The per-sender mechanics (what to suppress and why) are in Reputation Monitoring — suppression lists and Complaint Feedback Loops; this article covers the platform architecture: scopes, precedence, triggers, retention, and transfer. The primary public design evidence is AWS SES, which documents a complete four-level scheme; patterns are attributed accordingly.
The scope hierarchy (SES's four levels)
| Scope | Who it protects | Populated by | Customer-visible? | Retention |
|---|---|---|---|---|
| Global (platform-wide) | The platform itself, across all customers | Hard bounces from any customer's sending | No — not queryable, not editable, cannot be disabled | Escalating TTL, max 14 days |
| Account (per customer) | One customer account | Hard bounces and/or complaints from that account's sending; manual/bulk adds | Yes — full CRUD + bulk import/export | Indefinite until removed (90-day auto-purge if account is sending-paused) |
| Tenant (per sub-customer) | One tenant within an account | That tenant's own bounces/complaints, when tenant scope enabled | Yes — same API surface with a tenant parameter | Indefinite until removed; deleted with the tenant |
| Stream (per configuration set) | One sending workflow | Override layer only — redirects or disables checking/recording for sends using that stream | Yes — settings, not a separate list | n/a |
The general pattern: a short-lived, platform-controlled global backstop for provable delivery failures; long-lived, customer-controlled lists for everything else; and per-stream overrides for exceptions.
Global (platform-wide) suppression — SES pattern
- Trigger: hard bounce produced by any customer's send adds the address for all customers. Nothing else (no complaints) feeds the global list.
- Escalating retention: first hard bounce → suppressed for a short period, then auto-removed; each subsequent hard bounce re-adds it for a longer period; capped at 14 days on the list. This makes the global list a recency cache of known-bad addresses, not a permanent registry — deliberately avoiding permanent cross-customer suppression of addresses that may become valid again.
- Opacity: customers cannot query, add to, or disable it. The only signal is the synthetic bounce (below).
- Send behavior: a send to a globally suppressed address is accepted by the API but not transmitted; the platform generates a synthetic bounce (
bounceType=Permanent,bounceSubType=Suppressed). Critically, these synthetic bounces count toward the customer's bounce rate and daily quota — mailing suppressed addresses still costs reputation standing, preserving the incentive to clean lists. - Override: a customer's own account-level list takes precedence for their sending decisions; an address on the global list but absent from the customer's account list will be attempted if sent to — but a resulting bounce counts against the customer. (SES formerly offered a customer-facing global removal-request form; it was retired in favor of account-level lists.)
Design lesson: the global scope answers "should customer B be protected from an address that hard-bounced for customer A?" with yes, briefly, for delivery failures only — never for complaints, which stay within the account/tenant that earned them. That is the vendor answer to the cross-customer suppression question.
Account-level suppression — SES pattern
- Default-on: accounts created after November 25, 2019 get it enabled for both bounces and complaints; older accounts opt in (
PutAccountSuppressionAttributes). - Triggers (
SuppressedReasons):BOUNCE(hard bounces only — soft bounces never auto-suppress),COMPLAINT(FBL spam reports), or both. Each entry records its reason, the triggering feedback event, and the message that caused it. - Reason-match semantics: suppression only blocks a send when the entry's reason matches the account's configured reasons. Settings = Bounces only → entries with reason
Complaintare still mailed. Settings = Bounces and Complaints → both entry types block. - Metric treatment: sends blocked by the account list do not count toward the account's reputation bounce/complaint rates (they're surfaced as separate suppression counters) but do consume daily sending quota. The list is a reputation shield, not free volume.
- Propagation: hard-bounce entries added to the account list are also added to the global list — one customer's verified bad address briefly protects everyone.
- Retention: entries persist until explicitly removed. If the account's sending is paused (enforcement), the platform auto-deletes the account list after 90 days — unless sending is restored first.
- Known blind spot: Gmail provides no complaint feedback to SES, so Gmail "spam" clicks never populate the complaint suppression (true at every scope). Any suppression design must assume complaint data is structurally incomplete for some major providers — see Google Postmaster Tools for the aggregate substitute.
- Storage quirk worth copying deliberately or avoiding: addresses are stored case-preserved; matching at send time is case-insensitive, but management API calls require exact-case match.
- No size cap on the list itself.
Stream-level (configuration-set) overrides
A per-stream setting can override account suppression in three modes (SES's "suppression list options"):
- Inherit — stream uses account-level suppression unchanged.
- Disable entirely — stream bypasses all suppression ("override account settings" + suppression off). This is the platform-sanctioned mechanism for the transactional exception (a complainer must still get their receipts/tickets — see Complaint FBLs) and for deliberate re-verification sends.
- Custom reasons — stream enforces its own reason set, ignoring account settings (e.g. marketing stream suppresses on bounce+complaint while account default is bounce-only).
Addition-side scoping exists too: account suppression can be configured to only record new entries from sends tagged with specific configuration sets.
Tenant-level suppression — SES pattern (multi-tenant isolation)
By default all tenants in an account share the account-level list — one tenant's bounce/complaint suppresses the address for every tenant. Tenant-level suppression gives each tenant its own isolated list. Two settings, set together (both or neither): SuppressionScope (TENANT = use the tenant's own list; ACCOUNT = default, use the shared account list) and SuppressedReasons (BOUNCE, COMPLAINT, both, or empty = no checking and no recording).
Behavior matrix (as documented):
| Scope | Reasons | Send-time check | Recording |
|---|---|---|---|
| TENANT | BOUNCE, COMPLAINT | Tenant list, both reasons | Both → tenant list |
| TENANT | BOUNCE | Tenant list, bounces only | Bounces → tenant list |
| TENANT | COMPLAINT | Tenant list, complaints only | Complaints → tenant list |
| TENANT | (empty) | none | none |
| ACCOUNT | BOUNCE, COMPLAINT | Account list, both reasons | Both → account list |
| ACCOUNT | (empty) | none | none |
Key semantics:
- Precedence: configuration set > tenant > account. A stream override can change scope only, reasons only, or both, without touching tenant defaults.
- Scopes are exclusive, not layered: with scope
TENANT, the account list is skipped entirely — an address on the account list but not the tenant list is mailed. Isolation cuts both ways: the tenant escapes cross-tenant contamination and loses the shared protection. - Recording asymmetry: under tenant scope, hard bounces are recorded to the tenant list and the global list (never the account list); complaints go to the tenant list only. Delivery failures still feed the platform-wide backstop; behavioral signals stay tenant-private.
- Automatic un-suppression: a recipient's not-spam feedback (marking a previously reported message as not spam) auto-removes the matching
COMPLAINTentry from the tenant list. - Observability: tenant-suppression blocks are labeled — bounce type
Permanent/ subtypeOnTenantSuppressionList, diagnostic code "…on the suppression list for your tenant", and ases:tenant-nametag on bounce/complaint events for attribution. - Structure: exactly one list per tenant (1:1); region-specific; deleting the tenant deletes its suppression entries; blocked sends still count against sending quota; same case-sensitivity rule; same API operations as account level with a
TenantNameparameter (omitting it targets the account list — backward compatible).
Bulk transfer: import/export mechanics — SES pattern
Suppression portability matters when customers migrate between platforms (an incoming customer should import their previous suppression list before the first send; an outgoing one should be able to take it). SES's documented mechanics:
- Import (add): CSV (
address,REASON) or newline-delimited JSON ({"emailAddress":…,"reason":…}) from object storage; up to 100,000 addresses per import job, max 20 concurrent jobs; reasons limited toBOUNCE/COMPLAINT; per-job status with counts of processed and failed records and a failure file for reconciliation. - Bulk delete: same pipeline with a delete action; limit 10,000 addresses per job.
- Export: paginated listing API with
StartDate/EndDatefilters (entries added after/before a timestamp) — supports both full export and incremental sync. - Bulk operations require production (vetted) account status — a platform-abuse control, since bulk suppression import/removal is also a list-washing and probing vector.
Transfer considerations for an ESP: preserve the reason per entry across transfer (bounce vs complaint drive different override semantics); keep provenance (date, triggering event) where possible; and treat inbound "remove these from suppression" bulk jobs with more suspicion than adds. Legal constraints on transferring opted-out addresses (CAN-SPAM prohibits selling/transferring addresses a recipient opted out from, beyond suppression-purpose sharing) apply to the platform's handling of exported lists.
Architecture decisions the vendor evidence settles
- Scope complaints narrowly, bounces more broadly. In the SES design, complaints never leave the account/tenant that generated them; only hard bounces (objective delivery failures) propagate platform-wide, and even then with a ≤14-day TTL. A complainer for customer A is not auto-suppressed for customer B.
- Auto-suppression triggers are exactly two: hard bounce and FBL complaint. Soft bounces never auto-suppress at any scope (thresholded soft-bounce suppression, e.g. after repeated consecutive soft bounces, is a sender-side policy — see Reputation Monitoring). Unsubscribes are handled in list management, not the platform suppression layer, in this design.
- Every scope needs an override path with explicit, documented semantics (inherit / disable / custom), because the transactional exception is real — but make bypass a per-stream setting, not a per-send flag, so it stays auditable.
- Suppressed sends must stay visible and costed: emit distinct events (dedicated bounce subtypes / reason codes) so customers can see suppression working, and count suppressed attempts against quota so suppression isn't free volume; but exclude platform-suppressed sends from the customer's reputation metrics (the block prevented the harm).
- Retention differs by scope on purpose: platform-global = days (self-healing); customer/tenant = indefinite until acted on, with cleanup tied to account lifecycle (90-day purge after enforcement pause; deletion with the tenant). Honor-forever unsubscribe retention vs data-protection erasure is a separate legal design problem (flagged in CASL/PECR contexts) outside this article's sources — resolved on the legal side in GDPR and ESP Suppression Lists (when suppressing an address without the client's instruction turns the ESP from Data Processor into Data Controller, and why the minimal-retention suppression record survives an erasure request) and Right to Object and Erasure.
- Isolation is opt-in and explicit. The default is a shared account-wide list (maximum protection); tenant isolation is enabled per tenant when cross-tenant contamination outweighs shared protection — and the platform documents plainly that isolated tenants lose the account list's coverage.
For the tenant container itself (reputation policies, auto-pause), see Multi-Tenant ESP Architecture.
Sources
- https://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list.html
- https://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list-tenant-level.html
- https://docs.aws.amazon.com/ses/latest/dg/sending-email-global-suppression-list.html
- https://docs.aws.amazon.com/ses/latest/dg/tenants.html