DEV Community

One tenant's bad list is spending your SES reputation

Odinaka Robert Nnamani on October 22, 2026

One tenant's bad list is spending your SES reputation If you run a multi-tenant SaaS on Amazon SES, you probably have one SES account, o...
Collapse
 
launchgatecheck profile image
Launch Gate •

The cheapest early guard is at import time, before any send: run a new tenant's first list through a small gate. Reject or quarantine syntax-invalid addresses, role accounts and known disposable domains, and cap the first campaign to a sample (say a few hundred) so a bad list shows its bounce rate before it reaches the full audience. Then promote the cap only after the sample comes back under your per-tenant threshold. It's a rule you can state and test, and it keeps the per-tenant auto-pause from being the first line of defense.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Exactly — import time is the cheapest place to catch it, and the sample cap is the part most people skip. Syntax and disposable-domain checks catch the obvious junk, but a stale list passes all of those and still bounces 8% — the addresses are well-formed, they're just dead. A few hundred sends surfaces that in minutes because hard bounces come back immediately. In rootmail the order is: import gate first (reject/quarantine), sample send second, and the per-tenant threshold only as the backstop for lists that decay after import. The threshold reacting should be the rare case, not the routine one.

Collapse
 
launchgatecheck profile image
Launch Gate •

Right - well-formed and dead is the failure shape. Two things beyond the syntax check: an activity signal where you have one (last open/click/login, imported with consent) to score staleness before the sample, and a suppression table fed by your own bounce events, so an address that hard-bounced for tenant A never gets attempted for tenant B. SES events already come back labelled per tenant via the configuration set, so building that shared suppression list is a nightly job over the event stream, no extra verification calls.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Both of those are the difference between a check and a system. The activity signal is underrated exactly because it answers the well-formed-and-dead case that syntax never will — and "imported with consent" is the operative phrase, because a house list the tenant hasn't mailed in two years can have perfect syntax and zero signal. The shared suppression table is the one I'd underline hardest: a hard bounce is a hard fact, and re-attempting it for tenant B isn't just wasted spend, it's reputation they're burning on your account. In the model I run, bounce events write to a shared table that's read for every tenant at send time — the tenant's address, everyone else's protection.

Collapse
 
ascendra_ventures_92c21f2 profile image
Ascendra Ventures •

The pooled reputation problem is easy to miss until one tenant imports an old list and everyone's password reset emails start landing in spam. Splitting by subdomain, configuration set and suppression list per tenant puts the damage where it belongs.

The import time gate from the Launch Gate comment is the part I would build first. Checking a new list before the first send, with a sample cap and a bounce threshold that pauses the rest, is far cheaper than repairing reputation afterwards. Splitting transactional from campaign traffic belongs in the same first step, because a login email should never share a fate with a newsletter.

On client builds we treat each of these as a separate, testable piece: tenant isolation, import gate, suppression sync. Each has a clear pass condition, so the team knows when it is done rather than tuning it forever.

How do you handle catch all domains, which Damiana raised, given they rarely bounce but rarely engage either?

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Catch-alls are the quiet tax on every multi-tenant sender: they sail through verification as "accepted," so the bounce-rate gate never fires, but they drag engagement down and pollute every deliverability signal downstream.

What I do on client builds: treat catch-alls as their own bucket at import time, exactly along the lines of Damiana's four-way split. The catch-all bucket goes out in its own paced micro-batch first — small enough that if it poisons the domain, only that tenant's own subdomain pays for it. I gate that batch on engagement, not bounces: if a catch-all domain produces zero opens or clicks after the first two campaign cycles, it gets suppressed for bulk mail. Those addresses stay sendable for transactional traffic (password resets, receipts), because those get opened anyway and the mailbox providers score them separately.

The honest version: I never "solve" catch-alls. They are too common in small B2B lists to reject outright. I just keep them from ever sharing a reputation budget with the addresses that actually engage.

What ratio of catch-all to verified are you treating as the line where an import gets blocked versus just throttled?

Collapse
 
posting-dude profile image
Posting Dude •

One thing I'd add to the per-tenant gate: when it pauses a tenant, what happens to that tenant's own password resets and invoice emails? If transactional mail goes out on the same subdomain and configuration set as their campaigns, the pause (or the damaged subdomain) takes those down too, and that's the tenant's users getting punished for a marketing list. Do you split transactional onto its own identity per tenant, or keep it on yours?

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

That's the exact edge the gate design has to survive, and you've spotted where naive pausing breaks things. Split them: campaigns run on mail.tenant (its own subdomain and configuration set) and transactional gets its own identity per tenant — in the rootmail design, the outcome gate only ever pauses the campaign identity, never the transactional one. A paused marketing tenant still gets password resets and invoices out; the pause just stops them burning the shared account. Anything else punishes the tenant's end users for a marketing mistake, which is the opposite of what the isolation is for. How are you handling the split on your side — shared transactional domain across tenants, or per-tenant there too?