<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Steve</title>
    <description>The latest articles on DEV Community by Steve (@danby).</description>
    <link>https://dev.to/danby</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137839%2Fbfdaa57c-492a-4ad7-90ff-3131d6e95873.jpg</url>
      <title>DEV Community: Steve</title>
      <link>https://dev.to/danby</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danby"/>
    <language>en</language>
    <item>
      <title>One bad tenant can sink your whole email platform: what I learned building a reputation circuit breaker</title>
      <dc:creator>Steve</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:51:20 +0000</pubDate>
      <link>https://dev.to/danby/one-bad-tenant-can-sink-your-whole-email-platform-what-i-learned-building-a-reputation-circuit-5067</link>
      <guid>https://dev.to/danby/one-bad-tenant-can-sink-your-whole-email-platform-what-i-learned-building-a-reputation-circuit-5067</guid>
      <description>&lt;p&gt;I'm a solo founder in the UK building SentriSend, a security gateway that sits in front of transactional email (AWS SES / Resend). This week I shipped two features. Building them changed how I think about multi-tenant email, so here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody sees: subscription bombing
&lt;/h2&gt;

&lt;p&gt;Bots push a victim's address into hundreds of signup forms at once. The victim's inbox floods, and every site involved picks up complaints and spam-trap hits.&lt;/p&gt;

&lt;p&gt;The nasty part is that &lt;strong&gt;each individual site only sees one or two "signups"&lt;/strong&gt;. From where any single app is standing, nothing looks wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers of defence
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Per-sender limits.&lt;/strong&gt; Any one sender is stopped from repeatedly emailing the same address in a short window. Blocked requests get an HTTP 429 with a &lt;code&gt;Retry-After&lt;/code&gt; header, and no quota is charged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cross-sender detection.&lt;/strong&gt; A gateway sits in front of many senders, so it can see something no single app can: lots of &lt;em&gt;unrelated&lt;/em&gt; senders suddenly emailing the same address. That's a strong bombing signal. It triggers an alert first, then a temporary pause.&lt;/p&gt;

&lt;p&gt;In a live production test, six identical sends went to one address. Five were accepted, and the sixth came back &lt;code&gt;429 recipient_rate_limited&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; no plain addresses are stored. Recipients become keyed, pseudonymous identifiers, which are cleared automatically within hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson: one bad tenant can sink everyone
&lt;/h2&gt;

&lt;p&gt;While planning the next feature, I realised something uncomfortable. &lt;strong&gt;Every customer sends through my provider account.&lt;/strong&gt; One customer with a scraped or stale list could push bounces and complaints high enough to get the whole account suspended, and every other customer would go down with them.&lt;/p&gt;

&lt;p&gt;Worse, I had no feedback loop at all. Provider "accepted" isn't the same as delivered, and I wasn't receiving bounce or complaint reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a reputation circuit breaker
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listen.&lt;/strong&gt; A signed webhook receives delivery, bounce and complaint events. Each event is matched back to the API key that sent it, using hashed message IDs, and duplicates are counted once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure.&lt;/strong&gt; Each key's hard-bounce and complaint rates are tracked over a rolling window, with minimum sample sizes so a key isn't judged on three emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act.&lt;/strong&gt; Warn, then slow, then pause, all well before the provider's published limits. A paused key only returns through a manual review and a probation period. If the reputation check itself fails, sending stops rather than quietly carrying on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's also an account-wide alert, plus an alarm if webhook reports stop arriving, because silence shouldn't be mistaken for good health.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways if you run multi-tenant email
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If tenants share your sending account, &lt;strong&gt;their reputation is your reputation&lt;/strong&gt;. Isolate them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Sent" isn't "delivered".&lt;/strong&gt; Wire up provider webhooks early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail safely.&lt;/strong&gt; Decide deliberately which checks fail open and which fail closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the privacy policy from the code&lt;/strong&gt;, not from memory. Mine caught two mismatches before publishing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a free developer sandbox at sentrisend.com. If you run multi-tenant email, I'd love to hear how you stop one tenant hurting the rest.&lt;/p&gt;

</description>
      <category>email</category>
      <category>security</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How AWS SES bounce rates can put your account at risk, and how to reduce bad signups before sending</title>
      <dc:creator>Steve</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:50:14 +0000</pubDate>
      <link>https://dev.to/danby/why-aws-ses-puts-saas-accounts-on-probation-and-how-to-stop-the-5-bounce-cliff-55h6</link>
      <guid>https://dev.to/danby/why-aws-ses-puts-saas-accounts-on-probation-and-how-to-stop-the-5-bounce-cliff-55h6</guid>
      <description>&lt;p&gt;If you send transactional email through Amazon SES, your bounce rate matters more than most developers realise until it becomes a problem. This post explains how bad signups push it up, what AWS says about thresholds, and one way to reduce the problem before you send. I'm the author of SentriSend, the API mentioned below, so treat this as a builder's perspective rather than a neutral review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thresholds
&lt;/h2&gt;

&lt;p&gt;AWS publishes bounce-rate guidance for SES. As I understand it, AWS recommends keeping the rate low (around 2% or under), may place an account under review above 5%, and may pause sending at 10% or more. These numbers can change, so check the current &lt;a href="https://docs.aws.amazon.com/ses/" rel="noopener noreferrer"&gt;SES documentation&lt;/a&gt; rather than relying on this post. Other providers, including SendGrid, have their own rules.&lt;/p&gt;

&lt;p&gt;If sending is paused, everything that depends on your email stops: verification codes, password resets and receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean DNS doesn't prevent bounces
&lt;/h2&gt;

&lt;p&gt;SPF, DKIM and DMARC matter for deliverability, but they don't stop bounces caused by the addresses you send to. Three common sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated signups with disposable or made-up addresses&lt;/li&gt;
&lt;li&gt;Typos in real users' addresses&lt;/li&gt;
&lt;li&gt;Domains that don't exist or can't receive mail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a small sender, a modest number of bad addresses can move the percentage a lot, because the total volume is low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling it after the fact isn't enough
&lt;/h2&gt;

&lt;p&gt;Many guides cover SNS/SQS bounce notifications and suppression lists. Those are worth setting up, but they run after the message has been sent and the bounce has been recorded. To reduce bounces in the first place, you need to check the address before you send.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pre-send check
&lt;/h2&gt;

&lt;p&gt;SentriSend is an API that validates an address with one call before you send. It checks that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the address is well formed&lt;/li&gt;
&lt;li&gt;the domain isn't on a bundled list of about 133,000 known disposable domains (the list is built into the service at build time)&lt;/li&gt;
&lt;li&gt;the domain exists and can receive mail, using live DNS/MX lookups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the DNS result can't be determined, the address is allowed with MEDIUM risk, so a slow lookup doesn't block a genuine user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://sentrisend.com/api/v1/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_SENTRISEND_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "user@example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An allowed address returns HTTP 200:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"disposable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"risk_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LOW"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A disposable domain returns HTTP 400:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allowed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"disposable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DISPOSABLE_DOMAIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"risk_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your application reads the result and decides whether to continue sending through SES, Resend or whatever you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It doesn't confirm that an individual mailbox exists. A real domain with no such mailbox can still bounce.&lt;/li&gt;
&lt;li&gt;It doesn't detect bots. It validates the address, so pair it with CAPTCHA or rate limiting on your forms.&lt;/li&gt;
&lt;li&gt;It doesn't replace bounce handling. Keep your SNS/SQS suppression list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy
&lt;/h2&gt;

&lt;p&gt;In SentriSend's send audit records, the recipient, subject and message bodies are redacted. Customer account and billing data is stored separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The free tier is 100 requests a month, with no card needed. There's also a live simulator on the site, and the docs and a working example are public.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypk8j89ta47aenlbccks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypk8j89ta47aenlbccks.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://sentrisend.com" rel="noopener noreferrer"&gt;https://sentrisend.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/sentrisend/SentriSend" rel="noopener noreferrer"&gt;https://github.com/sentrisend/SentriSend&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've dealt with bad signups hitting your bounce rate, I'd like to hear how you handle it.&lt;/p&gt;

</description>
      <category>security</category>
      <category>aws</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
