DEV Community

Odinaka Robert Nnamani
Odinaka Robert Nnamani

Posted on

One tenant's bad list is spending your SES reputation

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, one or two verified identities, and every tenant's email flowing through them.

That means you also have exactly one sender reputation. And every tenant you onboard gets to spend it.

Here's the scenario that should worry you. A tenant imports a purchased list — or an old one, scraped from a CRM export nobody cleaned. They send a campaign. Hard bounces spike. A few recipients mark it as spam. SES watches two numbers across your whole account: bounce rate and complaint rate. Drift toward a 5% bounce rate or a 0.1% complaint rate and your account goes under review. Push further and SES pauses your sending.

Not that tenant's sending. Your sending. Every tenant. The good ones — the ones with clean lists and double opt-ins — wake up to the same dead deliverability as the one who caused it. Their password resets land in spam. Their invoices never arrive. And your dashboard, if it only tracks account-level numbers, tells you something is wrong long after all your tenants already know.

This isn't a monitoring problem you can alert your way out of. It's an architecture problem. Reputation on SES is pooled by default, so isolation has to be something you build — in three separate places.

1. Give each tenant its own sending identity

Reputation signals attach to identities: domains, MAIL FROM domains, IP pools. If all your tenants send from one domain on the shared IP pool, there is nothing for those signals to segment by.

What I build instead:

  • A sending subdomain per tenant — mail.tenant.yourdomain.com, or the tenant's own domain once they're serious — with its own SPF/DKIM alignment. A tenant who destroys their subdomain's standing has hurt their subdomain, not your root domain and not the tenant next door.
  • A configuration set per tenant. This is the unglamorous piece everything else hangs off. Configuration sets are how you tag a tenant's sends so every bounce, complaint, and delivery event comes back labelled with which tenant produced it. One shared configuration set means unattributed events, and unattributed events mean you're back to guessing.
  • Dedicated IP pools only for tenants who earn them. SES lets you assign configuration sets to dedicated IP pools. A tenant with steady, real volume can graduate to their own pool, fully out of the shared blast radius. Tenants without consistent volume stay on shared IPs — a cold dedicated IP with sporadic sends is its own deliverability problem.

2. Keep a per-tenant ledger, not an account-level blob

SES hands you an account-level suppression list and a stream of events. Most teams wire the events to an SNS topic, glance at a dashboard, and move on.

Split the stream by tenant (that's what the per-tenant configuration sets are for) and persist it as a ledger: every send, bounce, complaint, and suppression, recorded per tenant, append-only. Two things fall out of this:

  • Suppression becomes per-tenant too. An address that hard-bounced for Tenant A gets suppressed for Tenant A. On a shared account-level list, one tenant's typo'd import can silently block addresses your other tenants legitimately need to reach.
  • You get a reputation balance per tenant. Bounce rate, complaint rate, suppression growth — per tenant, over time. Not "the account looks bad this week," but "this tenant has been decaying for six days." That's the difference between an incident and a conversation.

3. Gate sending on per-tenant outcomes

Isolation without enforcement is just better observability of your own outage. The point of measuring per tenant is that you can act per tenant.

The pattern I use — and the one I keep coming back to when I talk to other builders about this — is an outcome gate per tenant:

  • Thresholds per tenant. When a tenant's bounce or complaint rate crosses your line, the gate pauses that tenant's sending automatically. The account stays healthy, the other tenants never notice, and the paused tenant gets a specific reason instead of your whole platform getting an SES review.
  • A ramp for new tenants. Nobody sends at full volume on day one. New tenants start on a paced schedule with a daily ceiling that rises as their outcome history proves out. A bad list then burns a small, contained allowance — not your account.
  • List hygiene before the first campaign, not after. The gate is the last line of defence. Checking an import for the obvious rot before it's allowed to send is the first one.

I sketched this gate design out with another builder in a GitHub thread recently — he'd been running deliverability as one global dashboard and treating every incident as platform-wide. He folded the per-tenant gate into his product's roadmap the same week and told me it filled a real gap in his design. It usually does. Most multi-tenant stacks built on SES have exactly this hole; they just haven't had the tenant yet who finds it.

What to check in your stack today

You don't need a rebuild to find out how exposed you are. Four questions:

  1. If one tenant spiked your bounce rate tonight, could you name them from your own data tomorrow morning — or only that "the account" had a bad day?
  2. Do your bounce and complaint events arrive tagged by tenant, or in one undifferentiated stream?
  3. Is suppression enforced per tenant, or is one tenant's bad import blocking addresses for everyone?
  4. What, mechanically, stops a tenant from sending their worst list at full volume right now? If the answer is "we'd notice," you don't have a control — you have a hope.

If you can't answer those cleanly, your tenants are sharing more than an SES account. They're sharing a reputation, a suppression list, and an outage waiting for its trigger.

I'm building these three pieces — per-tenant identities, per-tenant ledgers, per-tenant outcome gates — into rootmail, an email infrastructure layer on SES, and the beta is open if you want to see the approach in practice: https://rootmail.io/beta

Top comments (1)

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.