GLOSSARY

What is a Suppression List?

A suppression list is the set of recipient addresses a sending system must never contact again, assembled from unsubscribes, spam complaints, hard bounces, manual blocks and legal erasure requests, and checked in the sending worker immediately before every message leaves the queue.

Free forever plan · No credit card required · Cancel anytime

Quick definition

A suppression list is the set of recipient addresses a sending system must never contact again, assembled from unsubscribes, spam complaints, hard bounces, manual blocks and legal erasure requests, and checked in the sending worker immediately before every message leaves the queue.

In one line: the list of people you are not allowed to email, enforced at the last possible moment.

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.

Implementation

A schema that survives an audit.

Every column here exists because leaving it out caused a problem somewhere.

The table

CREATE TABLE email_suppressions (
    "Id"           BIGSERIAL   PRIMARY KEY,
    "WorkspaceId"  BIGINT      NOT NULL,
    "AddressHash"  TEXT        NOT NULL,  -- sha256 of the normalised address
    "Address"      TEXT        NOT NULL,  -- kept for support and export
    "Stream"       TEXT        NOT NULL,  -- marketing | lifecycle | transactional | all
    "Reason"       TEXT        NOT NULL,  -- unsubscribe | complaint | hard_bounce | manual | legal
    "SourceCode"   TEXT        NULL,      -- '5.1.1', the FBL id, the campaign id
    "SourceIp"     INET        NULL,      -- who asked, when it came from a form
    "CreatedAt"    TIMESTAMPTZ NOT NULL DEFAULT now(),
    "EndedAt"      TIMESTAMPTZ NULL       -- set on re-consent. never DELETE a row.
);

CREATE UNIQUE INDEX ux_email_suppressions_active
    ON email_suppressions ("WorkspaceId", "AddressHash", "Stream")
    WHERE "EndedAt" IS NULL;

The partial unique index is the trick: one active suppression per address per stream, while any number of historical rows can coexist underneath it.

The check, in the worker

-- runs once per message, immediately before handing it to SMTP
SELECT 1
  FROM email_suppressions
 WHERE "WorkspaceId" = @workspaceId
   AND "AddressHash" = @addressHash
   AND "EndedAt" IS NULL
   AND ("Stream" = @stream OR "Stream" = 'all')
 LIMIT 1;

-- normalisation, before hashing:
--   trim, lowercase the domain, lowercase the local part
--   on gmail.com and googlemail.com: strip dots and anything after '+'
--   never normalise plus tags on other providers, they are not universal
Source matrix

Six ways an address gets suppressed, and what each one means.

ReasonScopeDurationOperating note
UnsubscribeThe stream it came fromPermanentA withdrawal of consent, not a preference. Record which stream and which link produced it, because a newsletter opt-out is not a receipt opt-out.
Spam complaintEvery marketing and lifecycle streamPermanentArrives through a feedback loop, usually with the recipient address redacted by the provider, so match on the campaign identifier you embedded. Never email to ask why.
Hard bounceAll streamsPermanent, with the code keptStore the enhanced status code. A 5.1.2 caused by a typo domain can be justified for removal later; a 5.1.1 on a real domain cannot.
Repeated soft bounceAll streamsUntil re-verifiedSuppress after several consecutive failures across separate sends, not several retries of one send. One full mailbox is not a dead address.
Manual or legalEverything, including transactionalPermanentErasure requests and do-not-contact demands. This is the one category that outranks transactional necessity, and it needs an audit trail.
Provider-level blockAll senders on the platformManaged by the providerYour ESP keeps its own list. An address suppressed there will silently not be delivered even if your own list is clean, so reconcile the two.
Watch out for

Shared sending pools make this everybody problem.

On a shared IP pool, which is what almost every transactional provider gives you below a few hundred thousand messages a month, reputation is collective. One tenant mailing a purchased list produces complaints that depress delivery for every other sender on the same addresses, and none of them can see why.

That is why suppression enforcement belongs at the platform level rather than being left to each customer discipline. A send path that cannot be bypassed protects the tenant who configured it correctly from the one who did not.

Suppression lists: FAQ

The design questions that decide whether the list is a control or a decoration.

Per stream, with two exceptions. Someone who unsubscribes from a weekly digest still needs their password reset and their invoice, so marketing suppression must not silence transactional mail. The exceptions are spam complaints, which should stop everything except legally required messages, and erasure requests, which stop everything without exception. Model the stream as a column, not as a separate table, so a send-time check is one query.
In the worker that talks to SMTP, immediately before the send, not when the campaign audience is compiled. A campaign built on Monday and sent on Thursday will otherwise mail everyone who opted out in between, and a large batch takes long enough to run that someone will opt out while it is in flight. Checking twice, once at build and once at send, costs one indexed lookup and prevents the complaint that follows.
No. The record is your evidence that consent was withdrawn, and deleting it destroys the only proof you had that you honoured the request. If the person genuinely opts in again, write a new consent record with its own timestamp, source and IP, and let the suppression row remain as history with an end date. Under GDPR the burden of demonstrating consent sits with you, and an empty table demonstrates nothing.
Lowercase the domain always, since domains are case insensitive. Lowercase the local part too: it is formally case sensitive but every major provider treats it otherwise, and the alternative is mailing someone who thinks they unsubscribed. Store a hash of the normalised address alongside the plain value so you can index and compare without exposing the list, and decide deliberately whether to strip Gmail dots and plus tags, which honours intent but can over-match on other providers.
Never share address lists across tenants: one customer suppression list is their contact data, and copying it into another workspace is a data leak with no upside. What can and should be shared at platform level are reputation-driven controls, such as a domain or IP that is refusing all traffic, because those are facts about your infrastructure rather than about a customer contacts.
CAN-SPAM in the United States allows up to ten business days to honour an opt-out, GDPR expects it without undue delay, and the Gmail and Yahoo bulk sender rules require one-click unsubscribes to be processed within two days. Build to two days and every regime is satisfied. In practice a suppression write should be synchronous with the unsubscribe request, and the two-day allowance should only ever cover downstream systems catching up.
Ready to ship

One list. No way around it.

Pinlyx enforces suppression inside the sending worker, scopes it per stream so receipts still arrive, and keeps every record as evidence rather than deleting it.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.