Transactional mail, marketing blocklist
A single unsubscribe click can switch off the mail your product exists to send. The provider takes the send, reports success, and quietly drops it.
Every product that sends email eventually sends two kinds. There is the mail people might want, and there is the mail they can't work without. Your provider can't tell them apart, and by default it doesn't try.
It keeps one blocklist. One unsubscribe click, on anything you have ever sent, and every future email to that address stops. Including the ones that matter. Nothing errors either: the provider takes the send, says it worked, and quietly drops it.
It took us three support tickets to see that. Here is what we found, plus a second bug we hit on the way: unsubscribe links that fire when a security scanner opens them, with nobody clicking anything.
Fixed three times, never fixed
A customer told us one kind of email had stopped arriving. It was the transactional kind: a record of something they had done, and the replies to it. Without it they couldn't do their job.
Support found their address on the provider's suppression list, the global register it checks before every send, and removed it. A week later it was back. They removed it again. Then a second customer reported the same thing, then a spreadsheet of more addresses turned up.
The customer swore they had never unsubscribed from anything. Easy to assume they clicked something and forgot. They hadn't, and the unsubscribe was real anyway. Both were true.
The fix was a few fields on one outbound API call. The expensive part was that clearing the symptom took thirty seconds, and every occurrence had an obvious wrong explanation. Three times running, the cheap fix closed the ticket and taught us nothing.
Your provider keeps one list, and it does not know what matters
Providers keep a suppression list, and by default there is one per account. An address on it gets refused everything you send. The provider has no idea which of your mail is a newsletter and which is the thing your product exists to do.
So transactional and marketing mail were coupled through a shared blocklist nobody had chosen. Opting out of a weekly summary switched off mail the recipient needed to work. No config expressed that link, which is why nobody went looking for it.
That is an unpleasant kind of coupling, because the dependency isn't in your codebase. Grep finds nothing. No import, no call, no shared module. It is a row in a vendor's database, invisible until someone reports the damage.
And the unsubscribe link wasn't even ours. Our provider had subscription tracking switched on at the account level, appending its own link to every message. Gmail and Outlook read that and draw their own unsubscribe button next to the sender name. So a recipient can block all your mail through a control you never wrote, don't render, and won't find in your templates.
"Unsubscribe" is at least three different things
The word hides three different blast radiuses, and we had only ever built the widest one.
A global unsubscribe blocks everything from your domain until someone intervenes by hand. A group unsubscribe blocks one category you defined. A per-thread flag stops one conversation.
Ask what the person actually wanted. Someone sick of one noisy thread wants that thread to stop. They don't want to be cut off from everything you will ever send, and they weren't trying to break their own workflow. The default handed them the most destructive option and gave it the same label as the mildest one.
So: categorise your mail at the provider before you ship a single unsubscribe link. Most providers support it and it is cheap up front. Retrofitting means auditing every template you have ever sent.
Transactional mail needs one more step, because a global suppression matches on the address at send time and ignores whatever is in the message. Providers expose a flag for it. On SendGrid it is mail_settings.bypass_list_management, which exempts the message from suppression. Pick the narrowest variant your SDK offers: the blunt one also bypasses bounce and spam-report suppression, which you probably want to keep honouring.
Robots click every link you send
While digging into this, a colleague found a second bug of the same family. Our own unsubscribe endpoints were unauthenticated GET routes that did the unsubscribe the moment they were fetched. No confirmation page, no button.
Corporate email security fetches every link in an inbound message to check none are malicious. Link prefetchers in mail clients do the same. So an automated safety check can unsubscribe someone who never opened the email. Nobody clicked, and the state changed.
It is the oldest rule in HTTP and still the one most often broken at the edges. A GET must not change state. Email is the worst place to break it, because machines you have never heard of fetch your links on a schedule you don't control.
The pattern to use instead is RFC 8058 one-click unsubscribe. The mailbox provider sends a POST, and a bare GET on the same URL renders a confirmation page instead of acting. Humans still get one click. Scanners get nothing.
One thing worth noting. Both endpoints had the same bug, but the fallout wasn't symmetrical. The marketing one produced a support ticket, because people noticed their digests had stopped. The transactional one produced silence that looks exactly like the recipient choosing not to reply. That gets read as a people problem, escalated to the wrong team, and nobody checks the logs. So when you audit for this, rank by how badly the failure will be misread, not by how often it fires.
The silence was the expensive part
Nothing failed. No exception, no bounce, no alert. The provider took every send and returned success, then didn't deliver. From inside our system, the mail went out.
A suppressed send isn't an error to the provider. It is the list doing its job. So the one signal that would have caught this early sat in a vendor dashboard nobody had any reason to open.
Three things follow, and none of them are specific to email.
- Accepted isn't delivered. If a third party can quietly decline to do what you asked, reconcile what you sent against what happened. Most providers publish delivery events on a webhook. Consume them and alert on suppression drops for transactional mail.
- Count your suppressions. A growing suppression list is a growing population of customers who can't hear from you. Nothing surfaces that on its own, so it needs a number on a dashboard.
- A repeat fix is a signal. We cleared the same symptom three times. The third should have escalated by itself, because the recurrence was the most useful thing we knew. Each fix was just fast enough to feel like a resolution.
That last one is the hardest to act on. A support workflow that clears a symptom in thirty seconds will keep clearing it forever, and the speed is exactly what stops anyone asking why.
Tidying is not fixing
We had two unsubscribe mechanisms: the provider's automatic one and our own endpoints. Real debt, and everyone agreed it should go.
The proposal was to delete ours and standardise on the provider's, which was already live. One system instead of two. Good housekeeping.
It wouldn't have fixed anything. The same proposal named the provider's link as the likely source of the suppressions, then consolidated onto it without changing what it did. One click, human or robot, still blocks everything. Tidier plumbing, same flood.
Consolidation was the right call. It just couldn't carry the weight being put on it. When a cleanup and a fix point the same way, it is easy to ship the cleanup, feel finished, and notice months later that the incident rate never moved.
The test I use now: state the mechanism, then check your fix actually breaks it. Write the causal chain out in one sentence. If the change doesn't cut a specific link in that chain, it isn't the fix, however much nicer the code gets.
Same goes for evidence. Everyone, me included, spent a while arguing about which link the recipient had clicked. Nobody had established that they clicked one at all. The only hard fact was an address sitting on a list. Hardening the route we assumed had been used would have left the rest open, and the next ticket would have told us so.
Run this against your own setup
Each of these took minutes once we knew to look.
- List every category of mail you send and mark each one transactional or marketing. If you can't do that from config, your provider can't either.
- Check whether your provider auto-appends an unsubscribe link. It might be on by default and nowhere in your templates.
- Confirm transactional mail is exempt from suppression, or at least filed under its own unsubscribe group.
- Grep your routes for
GEThandlers that mutate state, starting with anything whose URL goes out in an email. - Check every unsubscribe path is
POST-only, with a confirmation page onGET. - Consume delivery-event webhooks and alert on suppression drops for transactional mail.
- Put the size of your suppression list on a dashboard someone actually looks at.
- Find out whether any support runbook clears a symptom without filing a cause, and count how often it has run.
That last one turned up more than any of the code changes did. A runbook step that quietly resolves the same ticket every fortnight is a bug report nobody has filed.
None of this was exotic. It was transactional mail sitting on a marketing blocklist, and nobody had ever said out loud which of our email was which.