What it means
Every sending system accumulates addresses it must stop using. Somebody unsubscribed. Somebody pressed report spam. A mailbox stopped existing. A lawyer sent an erasure request. Individually each of these is a single row; together they are the only thing standing between a healthy sender reputation and a blocklist entry.
A suppression list is that set, treated as infrastructure rather than as a marketing preference. The distinction matters because a preference is something a campaign tool consults when it feels like it, while infrastructure is something the send path cannot bypass. If a developer can write a script that emails a segment without touching the suppression check, the list is documentation, not a control.
Six sources, six different rules
The mistake is treating all suppressions as one category. They differ in scope, in duration and in what they let you do afterwards, and collapsing them either over-suppresses (people stop getting their invoices) or under-suppresses (people who complained keep receiving campaigns). The table below the article sets out the six sources you will actually encounter.
The two that catch teams out are complaints and blocks. A complaint arrives through a feedback loop and normally has the recipient address stripped out by the provider for privacy, so you can only match it if you embedded a campaign identifier in the original message. A block, meanwhile, is not a suppression at all: a 5.7.1 rejection is a statement about your sending reputation, and writing those recipients into the suppression list deletes clean addresses over a problem that lives on your side. See bounce rate for how to tell them apart from the status code alone.
Enforcement happens at send time
An audience compiled on Monday and sent on Thursday is three days out of date before the first message leaves. A batch of fifty thousand takes long enough to run that people unsubscribe while it is in flight, often because of the same campaign. So the check belongs in the worker immediately before the SMTP call, on the individual message, keyed on an index that makes it a sub-millisecond lookup.
Do it twice. Once when the audience is built, because filtering fifty thousand rows once is cheaper than fifty thousand individual lookups, and once in the worker, because that is the check that is actually correct. The second check is not redundant; it is the only one that reflects reality at the moment of sending.
Normalisation, and the aliasing question
Comparison fails on presentation. Lowercase the domain, always. Lowercase the local part too: the RFCs make it case sensitive, but no mainstream provider treats it that way, and the failure mode of being strict is that someone who unsubscribed keeps hearing from you.
Aliasing is a judgement call. On Gmail, dots in the local part are ignored and anything after a plus sign is a tag, so one mailbox has infinitely many spellings. Someone who wants to stay unsubscribed will not think to tell you about the other spellings, and someone who wants to re-subscribe with a tag has a legitimate reason to. Normalising Gmail addresses specifically honours intent and is what we would recommend; applying the same rule to every provider over-matches, because plus addressing is not universal and dots are significant elsewhere.
Store a hash of the normalised value alongside the plain address. The hash is what you index and compare, which means suppression checks in logs and in support tooling do not have to expose the address itself.
Suppression records are evidence
The strongest instinct when someone asks to be re-added is to delete the suppression row. Do not. That row is your record that a request was honoured, with a timestamp and a reason, and under GDPR the burden of demonstrating consent falls on you. Keep the row, add an end date, and write a fresh consent record for the new subscription with its own source, timestamp and IP address. A history of removal and re-consent is defensible. An empty table is not.
The same logic applies to imports. A CSV from a client is not consent, and running an import that quietly clears suppressions is how a compliant sender becomes a non-compliant one in a single afternoon. An import should be able to add contacts and should never be able to remove a suppression.
Related concepts
- List-Unsubscribe: the header that generates most of these records.
- Bounce rate: which codes belong here, and which must be kept out.
- Spam trap: the addresses a suppression list can never catch, because they never complain.
- Drip campaign: a sequence has to re-check suppression at every step, not only the first.
- Cold outreach: where do-not-contact records matter legally as well as operationally.
How Pinlyx handles it
Suppression is a workspace-scoped table with the stream as part of the key, so marketing opt-outs never silence transactional mail while complaints and erasure requests stop everything. Unsubscribes write synchronously from the one-click endpoint, hard bounces write from the return-path parser with their enhanced status code attached, and policy rejections in the 5.7 family deliberately do not write at all. The check runs again inside the sending worker against an indexed hash, so a sequence paused for a week cannot resume into somebody who left on day two. Records are never deleted, only ended, and imports have no permission to touch them.