<?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: Boyan Iliev</title>
    <description>The latest articles on DEV Community by Boyan Iliev (@boiliev).</description>
    <link>https://dev.to/boiliev</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%2F473176%2Fb39253d5-a467-4048-8e7e-fdfdf337696f.png</url>
      <title>DEV Community: Boyan Iliev</title>
      <link>https://dev.to/boiliev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boiliev"/>
    <language>en</language>
    <item>
      <title>No flippin way!</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Mon, 09 Nov 2026 11:06:28 +0000</pubDate>
      <link>https://dev.to/boiliev/-1bg2</link>
      <guid>https://dev.to/boiliev/-1bg2</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devopsdaily/we-built-opsville-a-free-pixel-art-town-where-you-fix-devops-incidents-5892" class="crayons-story__hidden-navigation-link"&gt;We Built Opsville, a Free Pixel-Art Town Where You Fix DevOps Incidents&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/devopsdaily/we-built-opsville-a-free-pixel-art-town-where-you-fix-devops-incidents-5892" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Boss fights simulate real incident response&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devopsdaily" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F382434%2F66e04ef9-7e4f-491c-997a-30f4a999d40c.jpg" alt="devopsdaily profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devopsdaily" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevOps Daily
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevOps Daily
                
                
              
              &lt;div id="story-author-preview-content-5003822" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devopsdaily" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F382434%2F66e04ef9-7e4f-491c-997a-30f4a999d40c.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevOps Daily&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devopsdaily/we-built-opsville-a-free-pixel-art-town-where-you-fix-devops-incidents-5892" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devopsdaily/we-built-opsville-a-free-pixel-art-town-where-you-fix-devops-incidents-5892" id="article-link-5003822"&gt;
          We Built Opsville, a Free Pixel-Art Town Where You Fix DevOps Incidents
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gamedev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gamedev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devopsdaily/we-built-opsville-a-free-pixel-art-town-where-you-fix-devops-incidents-5892" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;23&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devopsdaily/we-built-opsville-a-free-pixel-art-town-where-you-fix-devops-incidents-5892#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              4&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devops</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>9 Transactional Email Mistakes That Show Up in Production</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Fri, 06 Nov 2026 14:08:59 +0000</pubDate>
      <link>https://dev.to/boiliev/9-transactional-email-mistakes-that-show-up-in-production-k6o</link>
      <guid>https://dev.to/boiliev/9-transactional-email-mistakes-that-show-up-in-production-k6o</guid>
      <description>&lt;p&gt;Your checkout succeeds, but the receipt never arrives. A customer clicks “resend” and gets two copies. A password reset lands twenty minutes later, after the link has expired.&lt;/p&gt;

&lt;p&gt;A successful test email tells you little about how the integration behaves under failure. Production adds timeouts, retries, process crashes, DNS changes, and delivery events that arrive after your application has moved on.&lt;/p&gt;

&lt;p&gt;Here are nine mistakes worth checking before your users discover them. The examples use &lt;a href="https://smtpfa.st" rel="noopener noreferrer"&gt;SMTPfast&lt;/a&gt;, but the application patterns apply across email providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treating an API success response as proof of delivery
&lt;/h2&gt;

&lt;p&gt;This is an easy shortcut to ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markReceiptDelivered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider accepted your request. Your application immediately records that the customer received the receipt.&lt;/p&gt;

&lt;p&gt;Those are different events.&lt;/p&gt;

&lt;p&gt;Submission, recipient-server acceptance, inbox placement, and reading are separate stages. Even a successful SMTP handoff does not establish that a message appeared in someone’s inbox. SMTP defines responsibility for accepting and transporting a message; it does not provide a read receipt. &lt;a href="https://www.rfc-editor.org/rfc/rfc5321" rel="noopener noreferrer"&gt;RFC 5321&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; record what you actually know.&lt;/p&gt;

&lt;p&gt;Start with an application delivery record containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;notification_id
business_event_id
provider_email_id
submitted_at
delivered_at
bounced_at
last_error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the provider’s email ID when submission succeeds. Update delivery information from webhooks or reconciliation jobs.&lt;/p&gt;

&lt;p&gt;For example, SMTPfast exposes message details and delivery events through &lt;code&gt;GET /v1/emails/:id&lt;/code&gt;. That gives support a path from an order to the provider’s recorded outcome. &lt;a href="https://smtpfa.st/docs/emails" rel="noopener noreferrer"&gt;Emails API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A useful support answer is: “The recipient server accepted it at 14:32.” “Our API call succeeded” leaves most of the investigation unfinished.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Sending email inside the HTTP request
&lt;/h2&gt;

&lt;p&gt;A checkout handler often starts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now provider latency affects checkout latency.&lt;/p&gt;

&lt;p&gt;If sending fails after the order commits, the customer may see an error even though the purchase succeeded. If the process crashes between saving the order and sending the receipt, the order exists without any durable record that email remains to be sent.&lt;/p&gt;

&lt;p&gt;Moving the send into an untracked background promise leaves the crash problem intact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; write the business change and an &lt;strong&gt;outbox record&lt;/strong&gt; in the same database transaction.&lt;/p&gt;

&lt;p&gt;This simplified SQL illustrates the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;email_outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;notification_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;business_event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;notification_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'order_receipt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'pending'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A worker claims pending rows and submits the emails.&lt;/p&gt;

&lt;p&gt;Use a uniqueness constraint for the logical notification, such as &lt;code&gt;(business_event_id, kind, recipient_id)&lt;/code&gt;, so processing the same business event twice does not create two receipts.&lt;/p&gt;

&lt;p&gt;The worker also needs a recoverable claim or lease. If it dies mid-send, another worker must eventually be able to continue.&lt;/p&gt;

&lt;p&gt;That recovery introduces the next problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Retrying a timed-out send without an idempotency key
&lt;/h2&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Worker submits receipt
Provider accepts receipt
Response is lost
Worker records a timeout
Worker retries
Customer receives two receipts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout means your application does not know the outcome. It does not prove that the provider rejected the message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; assign an &lt;strong&gt;idempotency key&lt;/strong&gt; to the logical send and preserve it across attempts.&lt;/p&gt;

&lt;p&gt;For a provider endpoint that supports it, the request might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Idempotency-Key: receipt:order_4821:v1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate and store the key before the first attempt. Reuse the same request body when retrying that operation.&lt;/p&gt;

&lt;p&gt;This defeats the purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every retry becomes a new operation.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SMTPfast documents a 24-hour idempotency window for its batch endpoint, with a conflict response if the same key is reused with a different body. Check the exact endpoint’s contract before relying on it. &lt;a href="https://smtpfa.st/docs/emails" rel="noopener noreferrer"&gt;Batch sending documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your database uniqueness constraint prevents duplicate jobs. Provider idempotency addresses repeated submission after an uncertain outcome. You need both boundaries.&lt;/p&gt;

&lt;p&gt;Once the provider’s deduplication window expires, an unresolved send needs reconciliation or an explicit decision about duplicate risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Retrying until a useful email becomes a useless one
&lt;/h2&gt;

&lt;p&gt;A receipt delayed by ten minutes can still help.&lt;/p&gt;

&lt;p&gt;A password reset delivered after its token expires sends the user straight back to support.&lt;/p&gt;

&lt;p&gt;Treating every notification as “retry until successful” ignores the lifetime of the action inside the message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; give each job a deadline and check its business state before submitting it.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kind: password_reset
created_at: 12:00
token_expires_at: 12:15
send_before: 12:10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The five-minute margin is an application choice, not a delivery guarantee. It leaves some time for downstream delivery and the user to act.&lt;/p&gt;

&lt;p&gt;Before submission, check whether the job expired, the token was revoked, or a newer request superseded it.&lt;/p&gt;

&lt;p&gt;Classify failures too:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Worker action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invalid payload&lt;/td&gt;
&lt;td&gt;Stop retrying; fix the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid credentials&lt;/td&gt;
&lt;td&gt;Alert; stop repeated attempts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;Reschedule using provider guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary provider failure&lt;/td&gt;
&lt;td&gt;Retry with backoff and jitter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout with unknown outcome&lt;/td&gt;
&lt;td&gt;Retry using the same idempotency key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expired or superseded notification&lt;/td&gt;
&lt;td&gt;Mark expired or cancelled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SMTPfast supplies &lt;code&gt;Retry-After&lt;/code&gt; on rate-limit responses. Respect it rather than letting every worker retry immediately. &lt;a href="https://smtpfa.st/docs/rate-limits" rel="noopener noreferrer"&gt;Rate limits documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An application deadline stops future submissions. It cannot recall a message already handed to the mail system.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Treating DNS authentication as a launch-day task
&lt;/h2&gt;

&lt;p&gt;You verify the domain, send a test email, and close the ticket.&lt;/p&gt;

&lt;p&gt;Months later, someone migrates DNS or removes an unfamiliar record. Authentication starts failing while the application continues submitting messages successfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; make email DNS part of your infrastructure inventory and change review.&lt;/p&gt;

&lt;p&gt;Keep these roles clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; authorizes sending infrastructure for the envelope sender domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; signs messages using a signing domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; checks whether passing SPF or DKIM aligns with the visible &lt;code&gt;From&lt;/code&gt; domain and publishes a handling policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A passing SPF check for an unrelated domain does not, by itself, establish DMARC alignment. Google’s sender guidelines describe the alignment requirement for direct bulk email. &lt;a href="https://support.google.com/mail/answer/81126" rel="noopener noreferrer"&gt;Gmail sender guidelines&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a DNS migration, check the actual records and inspect authentication results on a received message.&lt;/p&gt;

&lt;p&gt;Useful checks include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short TXT bounce.your-domain.com
dig +short TXT _dmarc.your-domain.com
dig +short CNAME your-selector._domainkey.your-domain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace those names with the hostnames your provider supplies.&lt;/p&gt;

&lt;p&gt;Do not add a second SPF record at a hostname that already has one. Update the existing policy carefully. SMTPfast’s domain documentation covers this and the separate records used for its bounce subdomain. &lt;a href="https://smtpfa.st/docs/domains" rel="noopener noreferrer"&gt;Domains documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Logging bounces and complaints without acting on them
&lt;/h2&gt;

&lt;p&gt;A webhook arrives:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.bounced"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email_abc123"&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;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 handler writes a log line and returns success.&lt;/p&gt;

&lt;p&gt;Tomorrow, the application tries the same address again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; make delivery feedback change application behavior.&lt;/p&gt;

&lt;p&gt;A permanent bounce can mark an address as undeliverable and prompt the customer to correct it through another channel. A complaint should prevent continued sending according to your provider’s suppression policy.&lt;/p&gt;

&lt;p&gt;Temporary delivery problems need different handling. Avoid treating every delayed message as a permanently invalid address.&lt;/p&gt;

&lt;p&gt;SMTPfast automatically adds hard bounces and complaints to its suppression list and checks that list before sending. Your application should also use the outcome to stop creating futile jobs and explain the problem to users. &lt;a href="https://smtpfa.st/docs/suppressions" rel="noopener noreferrer"&gt;Suppressions documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do not automatically remove a suppression because a customer clicked “resend.” That button supplies no evidence that the underlying problem changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Applying the same unsubscribe rules to every email
&lt;/h2&gt;

&lt;p&gt;A customer unsubscribes from a newsletter. Your application flips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email_enabled = false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, they request a password reset. Your worker sees the flag and discards it.&lt;/p&gt;

&lt;p&gt;One boolean has combined several unrelated decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; model marketing preferences separately from delivery eligibility.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;marketing_opted_out
address_undeliverable
provider_suppressed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then classify each notification by its actual purpose.&lt;/p&gt;

&lt;p&gt;A newsletter opt-out should stop newsletters. A hard bounce still makes the address unusable for a receipt. Calling a promotion “transactional” does not make it an account notification.&lt;/p&gt;

&lt;p&gt;With SMTPfast, unlabelled sends default to marketing. Explicitly set the category for genuine transactional messages:&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;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transactional"&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;Marketing-only suppressions allow those messages through; hard-bounce and complaint suppressions block both categories. &lt;a href="https://smtpfa.st/docs/emails" rel="noopener noreferrer"&gt;Email categories&lt;/a&gt;, &lt;a href="https://smtpfa.st/docs/suppressions" rel="noopener noreferrer"&gt;suppression scopes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Test this distinction directly: unsubscribe a test contact from marketing, then verify that an eligible transactional notification still follows the intended path.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Assuming webhook delivery happens exactly once
&lt;/h2&gt;

&lt;p&gt;Your webhook handler updates the database, but the response never reaches the provider.&lt;/p&gt;

&lt;p&gt;The provider retries. Your handler applies the same side effect again.&lt;/p&gt;

&lt;p&gt;There is another failure window: the handler returns success, then crashes before saving the event. The provider considers delivery complete, but your application lost it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; authenticate the request, persist it durably, and make processing safe to repeat.&lt;/p&gt;

&lt;p&gt;A useful sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive raw request
→ Verify signature
→ Persist event with a deduplication key
→ Return success
→ Process asynchronously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SMTPfast, signature verification uses an HMAC over the raw request body. Parsing JSON and serializing it again can change the bytes being verified. &lt;a href="https://smtpfa.st/docs/webhooks" rel="noopener noreferrer"&gt;Webhook verification documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use a stable event identifier when the provider supplies one. Keep delivery-attempt identifiers separate. If the payload lacks an event ID, derive a deduplication strategy from the provider’s documented retry behavior.&lt;/p&gt;

&lt;p&gt;Also avoid replacing the entire message state with whichever event arrived last. A delayed event should not erase a later delivery outcome.&lt;/p&gt;

&lt;p&gt;SMTPfast retries unsuccessful webhook deliveries, so repeated requests are an expected integration path. &lt;a href="https://smtpfa.st/docs/webhooks" rel="noopener noreferrer"&gt;Webhook retry documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Testing the template while skipping the failure paths
&lt;/h2&gt;

&lt;p&gt;The preview looks good. The button works. The test message reaches your inbox.&lt;/p&gt;

&lt;p&gt;That covers rendering and one successful send.&lt;/p&gt;

&lt;p&gt;The worker, retry policy, and webhook handler need their own tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; exercise the boundaries where work can be lost or repeated.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Worker crashes before submission&lt;/td&gt;
&lt;td&gt;Job becomes eligible for processing again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider accepts; response is lost&lt;/td&gt;
&lt;td&gt;Retry preserves the idempotency key and body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider returns &lt;code&gt;429&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Worker reschedules according to retry guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same webhook arrives twice&lt;/td&gt;
&lt;td&gt;Side effects happen once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database is unavailable during webhook receipt&lt;/td&gt;
&lt;td&gt;Handler does not acknowledge an unpersisted event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reset job passes its deadline&lt;/td&gt;
&lt;td&gt;Worker marks it expired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipient has a hard-bounce suppression&lt;/td&gt;
&lt;td&gt;Worker does not keep resubmitting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Older event arrives late&lt;/td&gt;
&lt;td&gt;It does not overwrite newer delivery information&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use a fake provider for deterministic failure tests. Reserve real test sends for integration checks with addresses you control.&lt;/p&gt;

&lt;p&gt;Give support a practical lookup path too: business event → notification → provider email ID → delivery history. Record the template version and timestamps, while keeping reset tokens and full message bodies out of routine logs.&lt;/p&gt;

&lt;p&gt;For password resets and sign-in links, review click tracking as well. SMTPfast documents that tracked links pass through a redirect and their URLs are recorded; disable click tracking for links containing secrets. &lt;a href="https://smtpfa.st/docs/emails" rel="noopener noreferrer"&gt;Tracking documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next time you test transactional email, let one request time out after acceptance. Deliver a webhook twice. Stop the worker between submission and saving the response.&lt;/p&gt;

&lt;p&gt;Those tests tell you whether the integration can survive the failures your users will eventually encounter.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>email</category>
      <category>reliability</category>
    </item>
    <item>
      <title>The 12 Websites Every DevOps Engineer Should Bookmark in 2026</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:56:45 +0000</pubDate>
      <link>https://dev.to/boiliev/the-12-websites-every-devops-engineer-should-bookmark-in-2026-1k0n</link>
      <guid>https://dev.to/boiliev/the-12-websites-every-devops-engineer-should-bookmark-in-2026-1k0n</guid>
      <description>&lt;p&gt;A surprising amount of DevOps work starts with a small question. Is this database version still supported? Does this cron expression run when I think it does? What did that webhook actually send? Is the network having a bad day, or is it just our application?&lt;/p&gt;

&lt;p&gt;You can search for the answer every time. A few good bookmarks make the repetitive parts quicker.&lt;/p&gt;

&lt;p&gt;These twelve sites cover different parts of the job: learning a concept, checking a configuration, inspecting data, finding a package, or investigating an incident. DevOps Daily comes first because it covers the most ground. The rest earn their place by doing a specific job well.&lt;/p&gt;

&lt;p&gt;The sites and features below were checked in October 2026. Each has something useful available for free; that does not mean every feature is free or unlimited.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. DevOps Daily
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; engineers who want practical reading, interactive practice, and useful browser tools in one place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devops-daily.com/" rel="noopener noreferrer"&gt;DevOps Daily&lt;/a&gt; covers Docker, Kubernetes, Terraform, Git, Linux, CI/CD, networking, and the work around them. Alongside the articles, it has guides, exercises, quizzes, checklists, and more than 60 games and simulators.&lt;/p&gt;

&lt;p&gt;The interactive material is the reason to bookmark it. A written explanation of Kubernetes scheduling makes more sense when you can place pods on nodes and see resource constraints affect the result. The same applies to DNS resolution, load balancing, queues, caching, and deployment pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devops-daily.com/games" rel="noopener noreferrer"&gt;Simulators&lt;/a&gt; for exploring how systems behave, including terminal practice and incident scenarios.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devops-daily.com/exercises" rel="noopener noreferrer"&gt;Exercises&lt;/a&gt; that walk through tasks such as configuring a reverse proxy or troubleshooting Linux performance.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devops-daily.com/quizzes" rel="noopener noreferrer"&gt;Quizzes&lt;/a&gt; for finding gaps in your understanding before an interview or an unfamiliar project.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devops-daily.com/tools" rel="noopener noreferrer"&gt;Browser tools&lt;/a&gt; for tasks such as checking a CIDR block, reading a cron expression, or decoding a JWT.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://devops-daily.com/newsletters" rel="noopener noreferrer"&gt;newsletter&lt;/a&gt; if you prefer new material delivered to your inbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; the concept you can describe in an interview but still hesitate to debug. Spend ten minutes with the relevant simulator, then use an exercise to apply it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; it is a resource library rather than one prescribed course. A simulator also simplifies the real system; follow up with an actual environment when you need operational experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. endoflife.date
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; checking whether the software you run is approaching the end of support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://endoflife.date/" rel="noopener noreferrer"&gt;endoflife.date&lt;/a&gt; collects release and support timelines for operating systems, programming languages, databases, and infrastructure tools. It saves you from hunting through several release-policy pages just to plan an upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent tables across projects with very different release policies.&lt;/li&gt;
&lt;li&gt;Support milestones and release information in one place.&lt;/li&gt;
&lt;li&gt;Links back to the upstream sources behind the dates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; your next upgrade-planning session. Check the runtimes, database versions, and operating systems in the ticket before deciding which change can wait another quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; support is not always a single date. Extended support, commercial contracts, and distribution backports can change what applies to your deployment. Follow the linked vendor policy before making a production decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Crontab.guru
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; checking a cron expression before you schedule something expensive or important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crontab.guru/" rel="noopener noreferrer"&gt;Crontab.guru&lt;/a&gt; is a small editor that translates cron schedules into readable language. Paste an expression, change a field, and check whether the explanation matches your intention.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 9 * * 1-5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That schedules a job at 09:00 on weekdays in the scheduler's time zone. The last part matters as much as the expression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; a focused interface, quick feedback, and a collection of examples for common schedules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; five-field cron is not universal. Quartz, cloud schedulers, and other systems add fields or change the rules. Confirm the target scheduler's syntax, time zone, and daylight-saving behavior separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. CyberChef
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; answering “what is this data?” without writing a throwaway script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gchq.github.io/CyberChef/" rel="noopener noreferrer"&gt;CyberChef&lt;/a&gt; lets you build a recipe from operations such as decoding, decompression, hashing, and text conversion. The output of one operation becomes the input to the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; a test payload that was URL-encoded, then Base64-encoded. Put the decoding steps in a recipe and inspect the result after each operation. That is easier to debug than a growing chain of shell commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; recipes are reusable, processing runs in the browser, and the &lt;a href="https://github.com/gchq/CyberChef" rel="noopener noreferrer"&gt;open-source project&lt;/a&gt; can also be run locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; a recipe link can include the input as well as the operations. Inspect what you are sharing, and use synthetic data in examples. Being able to decode a value also says nothing about whether its contents are trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Qualys SSL Labs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; checking what an external client sees when it connects to your HTTPS endpoint.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.ssllabs.com/ssltest/" rel="noopener noreferrer"&gt;SSL Labs Server Test&lt;/a&gt; examines public TLS services. It reports certificate-chain issues, protocol support, cipher configuration, and compatibility details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; the morning after changing your ingress controller, reverse proxy, or certificate configuration. The certificate file on disk may be correct while the server still presents an incomplete chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; a detailed external assessment that goes well beyond checking a certificate's expiration date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; the service needs a publicly reachable endpoint. It cannot assess a private service behind your VPN. An excellent TLS grade also does not tell you whether application authentication or authorization is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Webhook.site
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; finding out what a webhook sender actually delivers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://webhook.site/" rel="noopener noreferrer"&gt;Webhook.site&lt;/a&gt; gives you a URL to receive requests and an interface for inspecting them. Point a test integration at it and look at the method, headers, query parameters, and body.&lt;/p&gt;

&lt;p&gt;A synthetic test is enough to get started:&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 &lt;span class="s1"&gt;'https://webhook.site/YOUR-UNIQUE-ID'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Test-Event: deployment.finished'&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;'{"environment":"demo","status":"success"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the placeholder with the unique URL the site gives you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; it separates two debugging questions: did the sender make the request, and can your application handle it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; anonymous URLs have request and expiration limits, and access is tied to possession of the URL. Use test events rather than production secrets or customer payloads. Receiving a signature header does not prove that the signature validates.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. regex101
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; understanding why a regular expression matches too much, too little, or nothing at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://regex101.com/" rel="noopener noreferrer"&gt;regex101&lt;/a&gt; shows matches and capture groups as you edit, with explanations of the pattern. It supports several engine flavors, including JavaScript, Python, Go, and PCRE2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; a log parser. Add a few valid lines, a malformed line, and a line that should never match. Build the pattern against all of them rather than polishing it around one convenient example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; immediate feedback, visible capture groups, substitutions, and explanations that make unfamiliar syntax easier to review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; choose the engine used by the target application. A pattern that works in PCRE2 may fail in Go or behave differently elsewhere. Keep the final test cases in your codebase so the parser stays correct after the browser tab closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. JSON Crack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; getting your bearings in a deeply nested response or configuration file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jsoncrack.com/" rel="noopener noreferrer"&gt;JSON Crack&lt;/a&gt; turns structured data into interactive graphs. It supports JSON, YAML, CSV, and XML, with formatting and conversion tools alongside the visual view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; a sanitized API response with several layers of arrays and objects. A graph can make the relationship between deployments, environments, and resource identifiers easier to follow than repeated folding and unfolding in an editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; a different view of the same data, plus image exports when you need to explain its shape to someone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; very large documents become very large graphs. Reduce the input to the relevant section first. A clear graph also does not validate the meaning of the configuration or replace a schema check.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Mermaid Live Editor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; making a quick diagram whose source can stay with the documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaid.live/" rel="noopener noreferrer"&gt;Mermaid Live Editor&lt;/a&gt; previews diagrams written as text. You can use it to sketch a request path, deployment flow, or sequence of events before adding the source to a README or runbook.&lt;/p&gt;

&lt;p&gt;For a simple request path, start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  client[Client] --&amp;gt; proxy[Reverse proxy]
  proxy --&amp;gt; app[Application]
  app --&amp;gt; database[(Database)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; the source is easy to review in Git, and changing a label does not require reopening a drawing application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; a diagram that renders in the latest editor may use syntax an older renderer does not understand. Check it in the place where readers will see it. Complex layouts can also take more effort than the text format initially suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Artifact Hub
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; finding cloud-native packages and checking their details before installation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://artifacthub.io/" rel="noopener noreferrer"&gt;Artifact Hub&lt;/a&gt; catalogs Helm charts and other artifacts, including policies, plugins, and operators. Its &lt;a href="https://artifacthub.io/docs/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; lists the supported package types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; comparing charts for a service you are about to deploy. Read the defaults, repository links, version history, and available security information before copying an install command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; package discovery and metadata in one place, with links to the material you need for a closer review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; discovery is the start of the decision. A verified publisher badge is not a security audit, and a recent chart release does not guarantee sensible defaults. Inspect the rendered manifests and image versions before installation.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. OpenSSF Scorecard
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; getting an initial view of an open-source project's security practices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://scorecard.dev/" rel="noopener noreferrer"&gt;OpenSSF Scorecard&lt;/a&gt; checks repository practices such as dependency pinning, workflow permissions, code review, and release controls. Its &lt;a href="https://github.com/ossf/scorecard" rel="noopener noreferrer"&gt;repository&lt;/a&gt; documents the checks and ways to run them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; a GitHub Action or infrastructure dependency you are considering adding to CI. Read the individual check results and their explanations, then investigate the findings that matter for the access you plan to give it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; it turns vague questions about maintenance and repository hygiene into specific things you can inspect. You can also use it through the CLI or GitHub Actions for repeatable checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; it assesses signals about a project's practices. It does not prove the code is safe or that its behavior fits your threat model. Check the age of any published results, and avoid reducing the decision to one number.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Cloudflare Radar
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; adding Internet-wide context to a network incident.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://radar.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Radar&lt;/a&gt; shows traffic trends, outages, and protocol signals observed through Cloudflare's network. It can help you investigate whether a disruption appears to extend beyond your own service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it for:&lt;/strong&gt; an incident affecting users in one country while your own service metrics look normal. Compare the region and time window with Radar's outage and traffic views, then follow up with your own probes and provider status information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt; an external perspective that an application dashboard cannot supply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt; it reflects Cloudflare's observations and coverage. A quiet Radar view does not rule out a local ISP, route, DNS, or application problem. Treat it as another piece of evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A useful bookmark saves you a search or gives you a better way to inspect a problem. DevOps Daily is the broadest starting point here because you can move from an explanation to a simulator, an exercise, or a small utility. The other sites help with the specific questions that come up around that work.&lt;/p&gt;

&lt;p&gt;Pick one you can use on your next ticket. If you want a quick starting point, open a &lt;a href="https://devops-daily.com/games" rel="noopener noreferrer"&gt;DevOps Daily simulator&lt;/a&gt; for something in your current stack and see which assumptions hold up when you change the inputs.&lt;/p&gt;

&lt;p&gt;Which website has earned a permanent place in your bookmark bar? Tell me in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sre</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>7 Resend Alternatives in 2026, With Real Prices and Tradeoffs</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:21:18 +0000</pubDate>
      <link>https://dev.to/boiliev/7-resend-alternatives-in-2026-with-real-prices-and-tradeoffs-2d2a</link>
      <guid>https://dev.to/boiliev/7-resend-alternatives-in-2026-with-real-prices-and-tradeoffs-2d2a</guid>
      <description>&lt;p&gt;I like &lt;a href="https://resend.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt;. The API is small and easy to read, React Email comes from the same team, there are official SDKs for nine languages, and you can send from four regions. Every plan, Free included, keeps 30 days of data. Open and click tracking work on Free too.&lt;/p&gt;

&lt;p&gt;So this is not a "Resend is bad" post. It's the list I'd send a friend who just hit one of these walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free plan stops at 100 emails a day.&lt;/strong&gt; That's a UTC calendar day, and received emails count toward it too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's nothing between $0 and $20.&lt;/strong&gt; Resend's own FAQ says so. Pro starts at $20 for 50,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newsletters are priced separately.&lt;/strong&gt; Broadcasts don't use your transactional quota. They're priced by contacts instead: the free tier covers 1,000 contacts, then Pro marketing starts at $40 a month for 5,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated IPs need Scale.&lt;/strong&gt; They're a $30/month add-on on Scale (from $90/month), once you send more than 3,000 emails a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your account data stays in the US.&lt;/strong&gt; Picking the Ireland region changes where mail is sent from, not where logs and metadata are stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly billing only.&lt;/strong&gt; No annual plans and no annual discount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of that bothers you, stay on Resend. If one of them does, read on.&lt;/p&gt;

&lt;p&gt;Every price below is from the provider's own pricing page, as of October 2026. Prices change, so check before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Paid plans from&lt;/th&gt;
&lt;th&gt;About 50K emails/month&lt;/th&gt;
&lt;th&gt;SMTP&lt;/th&gt;
&lt;th&gt;Inbound email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resend (for reference)&lt;/td&gt;
&lt;td&gt;3,000/mo, 100/day&lt;/td&gt;
&lt;td&gt;$20 (50K)&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, counts toward quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMTPfast&lt;/td&gt;
&lt;td&gt;3,000/mo, 100/hour&lt;/td&gt;
&lt;td&gt;$9 (10K)&lt;/td&gt;
&lt;td&gt;$19&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailtrap&lt;/td&gt;
&lt;td&gt;4,000/mo, 150/day&lt;/td&gt;
&lt;td&gt;$15 (10K)&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmark&lt;/td&gt;
&lt;td&gt;100/mo&lt;/td&gt;
&lt;td&gt;$15 (10K)&lt;/td&gt;
&lt;td&gt;$55&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Pro and Platform plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon SES&lt;/td&gt;
&lt;td&gt;None (AWS credits for new accounts)&lt;/td&gt;
&lt;td&gt;Pay as you go&lt;/td&gt;
&lt;td&gt;$5 to $8&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useSend&lt;/td&gt;
&lt;td&gt;3,000/mo, 100/day&lt;/td&gt;
&lt;td&gt;$10 minimum usage&lt;/td&gt;
&lt;td&gt;$20 (transactional)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not in the docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailgun&lt;/td&gt;
&lt;td&gt;100/day&lt;/td&gt;
&lt;td&gt;$15 (10K)&lt;/td&gt;
&lt;td&gt;$35&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (routes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SendGrid&lt;/td&gt;
&lt;td&gt;60-day trial, 100/day&lt;/td&gt;
&lt;td&gt;$19.95 (50K)&lt;/td&gt;
&lt;td&gt;$19.95&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (Inbound Parse)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. SMTPfast
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that like Resend's API and want a smaller bill at low and mid volumes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://smtpfa.st" rel="noopener noreferrer"&gt;SMTPfast&lt;/a&gt; is an email API built on Amazon SES. It's on this list because its documented API takes Resend's request format, so the move can be as small as a new base URL and API key. The &lt;a href="https://smtpfa.st/docs/resend" rel="noopener noreferrer"&gt;Resend compatibility docs&lt;/a&gt; list the fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free: 3,000 emails a month, no card&lt;/li&gt;
&lt;li&gt;Starter: $9 for 10,000&lt;/li&gt;
&lt;li&gt;Growth: $19 for 50,000&lt;/li&gt;
&lt;li&gt;Scale: $49 for 200,000&lt;/li&gt;
&lt;li&gt;Yearly billing gets you two months free. For one busy month, prepaid credits cost $1 per 1,000 extra emails ($5 minimum), so you don't have to change plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Broadcasts are part of the same plan, not a second bill. They use your monthly email quota, with caps on contacts (1,000 on Free up to 100,000 on Scale) and on broadcasts per month (2 up to 200).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API, SMTP and tooling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;REST API at &lt;code&gt;https://smtpfa.st/api/v1&lt;/code&gt;. Sending, batch, contacts, &lt;a href="https://smtpfa.st/docs/templates" rel="noopener noreferrer"&gt;templates&lt;/a&gt;, &lt;a href="https://smtpfa.st/docs/inbound" rel="noopener noreferrer"&gt;inbound&lt;/a&gt; and inboxes follow Resend's field names and shapes.&lt;/li&gt;
&lt;li&gt;SMTP bridge at &lt;code&gt;smtp.smtpfa.st&lt;/code&gt;, port 587 or 2525, STARTTLS, with your API key as the password.&lt;/li&gt;
&lt;li&gt;A hosted &lt;a href="https://smtpfa.st/docs/mcp" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; that you connect with OAuth or an API key.&lt;/li&gt;
&lt;li&gt;A CLI (&lt;code&gt;npx smtpfast&lt;/code&gt;) where every API endpoint is a command.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://registry.terraform.io/providers/smtpfast/smtpfast" rel="noopener noreferrer"&gt;Terraform provider&lt;/a&gt; for domains, API keys, webhooks, templates and more.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://smtpfa.st/docs/webhooks" rel="noopener noreferrer"&gt;Webhooks&lt;/a&gt; and inbound email on paid plans, and teams with 3 to 25 seats.&lt;/li&gt;
&lt;li&gt;Per-email tracking switches (&lt;code&gt;track_clicks&lt;/code&gt;, &lt;code&gt;track_opens&lt;/code&gt;, or a &lt;a href="https://smtpfa.st/docs/emails#tracking" rel="noopener noreferrer"&gt;per-link opt-out&lt;/a&gt;), so you can keep a password-reset link out of click tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SDKs:&lt;/strong&gt; none of its own yet. You use fetch, the CLI or SMTP. The official &lt;code&gt;resend&lt;/code&gt; Node package also works if you point it at SMTPfast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resend&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// resend 6.13.0 and newer take a baseUrl option&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Resend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMTPFAST_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://smtpfa.st/api/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello@yourapp.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Thanks for signing up.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran &lt;code&gt;domains.list&lt;/code&gt;, &lt;code&gt;emails.list&lt;/code&gt; and &lt;code&gt;emails.get&lt;/code&gt; through &lt;code&gt;resend&lt;/code&gt; 6.34.1 this way, and all three worked. I did not send email through it, so do one test send before you switch production traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a younger product with a smaller community and fewer integrations than the others here.&lt;/li&gt;
&lt;li&gt;The Free plan is thinner than Resend's: 1 domain, no webhooks, no inbound, and at most 100 emails an hour. Open and click tracking start on Growth.&lt;/li&gt;
&lt;li&gt;No dedicated IPs, and no choice of sending region.&lt;/li&gt;
&lt;li&gt;At exactly 100,000 emails a month, Scale ($49) costs more than Mailtrap ($30), SendGrid ($34.95) or Resend ($35). At 200,000 it's $49 against Resend's $160 Scale tier.&lt;/li&gt;
&lt;li&gt;No drag-and-drop editor. Templates are HTML or Markdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; probably the smallest code change here, since the request format matches, but test it before you trust it. After the base URL and key, check three things. Webhook signatures are an HMAC in &lt;code&gt;X-SMTPfast-Signature&lt;/code&gt;, not Svix headers. Attachments need base64 &lt;code&gt;content&lt;/code&gt;, not a remote &lt;code&gt;path&lt;/code&gt;. Tracking is an account setting with a per-email override, not a per-domain setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Mailtrap
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; product teams that want transactional and marketing mail kept apart from day one, plus a bigger free tier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mailtrap.io" rel="noopener noreferrer"&gt;Mailtrap&lt;/a&gt; is a sending platform with an Email API and an SMTP relay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free: 4,000 emails a month, up to 150 a day&lt;/li&gt;
&lt;li&gt;Basic: $15 for 10,000, $20 for 50,000, $30 for 100,000&lt;/li&gt;
&lt;li&gt;Business: from $85 for 100,000, with a dedicated IP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional and bulk streams run on isolated IP pools on every plan, Free included.&lt;/li&gt;
&lt;li&gt;An official MCP server, plus &lt;a href="https://github.com/mailtrap/mailtrap-skills" rel="noopener noreferrer"&gt;seven agent skills&lt;/a&gt; for sending, templates and domain setup.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.mailtrap.io/getting-started/email-api-smtp" rel="noopener noreferrer"&gt;Eight official SDKs&lt;/a&gt; (Node.js, Python, PHP, Ruby, Go, Java, .NET and Elixir), an &lt;code&gt;llms.txt&lt;/code&gt;, and Vercel and Supabase integrations.&lt;/li&gt;
&lt;li&gt;The free tier includes the API, webhooks, templates and inbound email.&lt;/li&gt;
&lt;li&gt;ISO 27001 and SOC 2 Type II.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short log retention on self-serve plans: 3 days on Free, 5 on Basic, 15 on Business.&lt;/li&gt;
&lt;li&gt;Dedicated IPs start on Business at $85 a month.&lt;/li&gt;
&lt;li&gt;150 emails a day on Free is tight for a launch-day signup spike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; a moderate rewrite. You &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;https://send.api.mailtrap.io/api/send&lt;/code&gt;, and addresses are objects (&lt;code&gt;{ "email": "..." }&lt;/code&gt;), with &lt;code&gt;to&lt;/code&gt; as an array of them. Over SMTP it's only new credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Postmark
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; transactional-only sending, such as login codes and receipts, with long log retention. Postmark's own pitch is deliverability and speed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://postmarkapp.com" rel="noopener noreferrer"&gt;Postmark&lt;/a&gt; focuses on transactional email and keeps broadcast mail on separate streams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free: 100 emails a month, no overage&lt;/li&gt;
&lt;li&gt;From 10,000 emails: Basic $15, Pro $16.50, Platform $18&lt;/li&gt;
&lt;li&gt;At 50,000: $55, $60.50 and $66&lt;/li&gt;
&lt;li&gt;Extra emails: $1.80, $1.30 and $1.20 per 1,000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;45 days of activity data by default. Pro and Platform can extend it to 365 days.&lt;/li&gt;
&lt;li&gt;Message Streams keep transactional and broadcast mail apart, with a separate SMTP host for broadcasts.&lt;/li&gt;
&lt;li&gt;Official libraries for Node.js and TypeScript, Python, Ruby, PHP, .NET and Java.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The highest price at 50,000 emails on this list: $55, where several others charge about $20.&lt;/li&gt;
&lt;li&gt;No tier between the free 100 emails and 10,000.&lt;/li&gt;
&lt;li&gt;Inbound processing needs Pro or Platform.&lt;/li&gt;
&lt;li&gt;Dedicated IPs start at $50 a month per IP and need 300,000+ emails a month on Pro or higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; moderate. &lt;code&gt;POST https://api.postmarkapp.com/email&lt;/code&gt; with an &lt;code&gt;X-Postmark-Server-Token&lt;/code&gt; header and PascalCase fields (&lt;code&gt;From&lt;/code&gt;, &lt;code&gt;To&lt;/code&gt;, &lt;code&gt;HtmlBody&lt;/code&gt;, &lt;code&gt;TextBody&lt;/code&gt;, &lt;code&gt;MessageStream&lt;/code&gt;). SMTP is a credentials swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Amazon SES
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; AWS teams with real volume and someone who will own the setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/ses/" rel="noopener noreferrer"&gt;Amazon SES&lt;/a&gt; is the raw sending service. SMTPfast and useSend on this list both run on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A la carte: $0.10 per 1,000 emails, plus $0.12 per GB of attachments&lt;/li&gt;
&lt;li&gt;New SES accounts now start on the Essentials plan at $0.16 per 1,000 with no monthly fee. You can switch to a la carte at any time.&lt;/li&gt;
&lt;li&gt;No standing free tier. New AWS customers get up to $200 in Free Tier credits.&lt;/li&gt;
&lt;li&gt;Dedicated IPs: $24.95 a month each (standard)&lt;/li&gt;
&lt;li&gt;Inbound: $0.10 per 1,000 emails plus $0.09 per 1,000 incoming chunks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lowest price per email here by a wide margin.&lt;/li&gt;
&lt;li&gt;An SMTP interface plus the AWS SDKs.&lt;/li&gt;
&lt;li&gt;Virtual Deliverability Manager (included on Essentials) has a searchable message table with each email's event history for the last 30 days.&lt;/li&gt;
&lt;li&gt;An account-level suppression list for hard bounces and complaints, on by default for accounts created since late 2019.&lt;/li&gt;
&lt;li&gt;Event publishing to SNS, EventBridge, CloudWatch or Firehose, and IAM for access control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New accounts start in a sandbox: verified recipients only, 200 messages per 24 hours, 1 per second. AWS gives a first answer to a production access request within 96 hours.&lt;/li&gt;
&lt;li&gt;No app-style webhooks. If your app needs to react to bounces or opens, you wire events into SNS or EventBridge and write the handler yourself.&lt;/li&gt;
&lt;li&gt;SMTP credentials are per region.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; more setup than the others. You move to &lt;code&gt;SendEmail&lt;/code&gt; through the AWS SDK (or SES SMTP credentials), set up IAM and configuration sets, and connect your own event handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. useSend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; developers who want an open-source, Resend-style platform they can self-host.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://usesend.com" rel="noopener noreferrer"&gt;useSend&lt;/a&gt; calls itself an "open source alternative to Resend, Sendgrid, Postmark etc." It's AGPL-3.0, has about 4,700 GitHub stars, and sends through Amazon SES.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud Free: 3,000 emails a month, 100 a day, 1 domain, 1 team member&lt;/li&gt;
&lt;li&gt;Cloud Paid: $10 minimum usage a month (it comes with $10 of credit). Transactional email is $0.0004 each ($0.40 per 1,000) and marketing email is $0.001 each ($1 per 1,000).&lt;/li&gt;
&lt;li&gt;Self-hosted: free software, and you pay AWS for sending&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;REST API, an SMTP relay at &lt;code&gt;smtp.usesend.com&lt;/code&gt; (ports 465, 587, 2465, 2587), and SDKs for Node.js, Python and Go.&lt;/li&gt;
&lt;li&gt;Campaigns, contact books and signed webhooks.&lt;/li&gt;
&lt;li&gt;A guide for using it with React Email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-hosting means running Postgres, Redis, SES and SNS, plus a GitHub app for login.&lt;/li&gt;
&lt;li&gt;AGPL-3.0 matters if you change the code and run it as a service for others.&lt;/li&gt;
&lt;li&gt;A small, bootstrapped project. I found no inbound email in the docs.&lt;/li&gt;
&lt;li&gt;The free tier has the same 100-a-day cap as Resend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; the SDK call looks familiar (&lt;code&gt;usesend.emails.send({ to, from, subject, html })&lt;/code&gt;). The REST fields are camelCase (&lt;code&gt;replyTo&lt;/code&gt;, &lt;code&gt;scheduledAt&lt;/code&gt;), so raw HTTP code needs edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Mailgun
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; apps that receive a lot of email and need routing rules, or that want to validate addresses before sending.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mailgun.com" rel="noopener noreferrer"&gt;Mailgun&lt;/a&gt; offers REST and SMTP sending, with inbound routing built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free: 100 emails a day, 1 domain, 1 day of logs, 1 inbound route&lt;/li&gt;
&lt;li&gt;Basic: from $15 for 10,000, $1.80 per 1,000 extra, 1 day of logs, 5 inbound routes&lt;/li&gt;
&lt;li&gt;Foundation: $35 for 50,000 after a free first month, 1,000 domains, 5 days of logs, full inbound routing&lt;/li&gt;
&lt;li&gt;Scale: $90 for 100,000, 30 days of logs, dedicated IP pools, 5,000 email validations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inbound routes with full routing from Foundation up.&lt;/li&gt;
&lt;li&gt;An email validation API.&lt;/li&gt;
&lt;li&gt;Six official SDKs: Go, Node.js, PHP, Java, Ruby and Python.&lt;/li&gt;
&lt;li&gt;One dedicated IP included from Foundation 100k up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 day of logs on Free and Basic.&lt;/li&gt;
&lt;li&gt;Extra emails cost $1.80 per 1,000 on Basic, twice Resend's $0.90.&lt;/li&gt;
&lt;li&gt;The send endpoint takes &lt;code&gt;multipart/form-data&lt;/code&gt;, not JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; moderate. You send form fields to &lt;code&gt;/v3/&amp;lt;your-domain&amp;gt;/messages&lt;/code&gt;, and inbound moves to routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. SendGrid (Twilio)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that need subusers, SSO and dedicated IPs for many brands or teams, or that already use Twilio.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.twilio.com/en-us/sendgrid" rel="noopener noreferrer"&gt;SendGrid&lt;/a&gt; is Twilio's email API, with a separate marketing product next to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (as of October 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free trial: 100 emails a day for 60 days&lt;/li&gt;
&lt;li&gt;Essentials: $19.95 for 50,000, $34.95 for 100,000&lt;/li&gt;
&lt;li&gt;Pro: from $89.95 for 100,000, with dedicated IPs&lt;/li&gt;
&lt;li&gt;Marketing Campaigns is a separate product with separate pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stands out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seven official libraries: C#, Go, Java, Node.js, PHP, Python and Ruby.&lt;/li&gt;
&lt;li&gt;The Inbound Parse webhook for receiving mail.&lt;/li&gt;
&lt;li&gt;Subuser management and SSO on Pro and up, and an EU endpoint for EU regional subusers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No permanent free tier, only the 60-day trial.&lt;/li&gt;
&lt;li&gt;Searchable activity history is 3 days on Essentials and 7 days on Pro.&lt;/li&gt;
&lt;li&gt;Dedicated IPs start on Pro at $89.95.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switching from Resend:&lt;/strong&gt; over SMTP it's a credentials swap. The v3 API is a bigger change: the Mail Send body uses a &lt;code&gt;personalizations&lt;/code&gt; array, &lt;code&gt;from&lt;/code&gt; as an object, and &lt;code&gt;content&lt;/code&gt; as a list of type and value pairs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You like Resend's API but want a paid plan below $20, or one plan for app email and newsletters:&lt;/strong&gt; SMTPfast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a bigger free tier, isolated streams and agent tooling:&lt;/strong&gt; Mailtrap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You send only transactional mail and want long log retention:&lt;/strong&gt; Postmark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You send a lot, live in AWS, and have someone to run it:&lt;/strong&gt; Amazon SES.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want open source and to own the stack:&lt;/strong&gt; useSend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You process a lot of inbound mail or validate lists:&lt;/strong&gt; Mailgun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need subusers and SSO across brands, or you already use Twilio:&lt;/strong&gt; SendGrid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;None of the Resend limits above affect you:&lt;/strong&gt; stay on Resend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two tips before you move. Price the plan you'll be on in six months, not the one on the headline. And send to a test domain first: check bounces, webhooks and signature checks before production traffic goes through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Resend raised the bar for email developer experience. If the 100-a-day free cap, the jump to $20, or the separate marketing pricing is what's pushing you to look, SMTPfast is a cheap one to try first, since its request format matches Resend's. Run it on a test domain before you move production. If you need something specific, like Postmark's long retention or SES pricing at scale, the list above should point you to it.&lt;/p&gt;

&lt;p&gt;What are you sending with right now, and what made you switch? Tell me in the comments.&lt;/p&gt;

</description>
      <category>email</category>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>Your App Can Receive Email Too: Build an Email-to-Ticket Pipeline with TypeScript</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:49:40 +0000</pubDate>
      <link>https://dev.to/boiliev/your-app-can-receive-email-too-build-an-email-to-ticket-pipeline-with-typescript-4ihh</link>
      <guid>https://dev.to/boiliev/your-app-can-receive-email-too-build-an-email-to-ticket-pipeline-with-typescript-4ihh</guid>
      <description>&lt;p&gt;Most application email tutorials stop after &lt;code&gt;sendEmail()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That covers password resets, receipts, and alerts. But email can also be an input to your application: a message sent to &lt;code&gt;support@inbound.example.com&lt;/code&gt; can create a ticket, and a reply can join the same conversation. People already know how to send a message, attach a file, and reply. Your application can meet them in that workflow.&lt;/p&gt;

&lt;p&gt;To explore that idea, this post walks through &lt;strong&gt;Mailroom&lt;/strong&gt;, a small support inbox you can run and adapt. SMTPfast receives and parses the email; the TypeScript app turns it into a ticket that an agent can answer in the browser. Support tickets give us a concrete example, but the same approach can handle emailed bug reports, document submissions, or replies to notifications in a SaaS app.&lt;/p&gt;

&lt;p&gt;Mailroom uses Fastify for its HTTP routes, PostgreSQL for tickets and pending work, and SMTPfast for inbound email and threaded replies. A server-rendered inbox makes the conversation visible without needing a separate frontend application.&lt;/p&gt;

&lt;p&gt;The app runs locally and checks SMTPfast for new messages. You can test the complete conversation without deploying a server or setting up an HTTPS tunnel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-DevOps-Daily/smtpfast-email-to-ticket" rel="noopener noreferrer"&gt;SMTPfast Email-to-Ticket on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have built a Node.js API and are comfortable with basic TypeScript, HTTP requests, and SQL, you have the background needed to follow along. No prior experience with receiving email is assumed. To run the example, you will need Node.js, Docker Compose, a domain you can configure, and an SMTPfast plan that includes inbound email.&lt;/p&gt;

&lt;p&gt;For the runnable demo, follow the setup and test the email conversation. Continue with the implementation sections to understand queues, threading, and safe rendering. The webhook section is an optional extension for a deployed app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when an email arrives?
&lt;/h2&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%2F4i5s3b8an99wqq5n0buq.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%2F4i5s3b8an99wqq5n0buq.png" alt="Email flows from a sender to SMTPfast, through polling or an optional signed webhook into a PostgreSQL queue, then through the worker into tickets read by the agent UI. Replies go through SMTPfast." width="800" height="1008"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SMTPfast accepts the message, scans it, parses it, and exposes it through the receiving API. Mailroom can discover it by &lt;strong&gt;polling&lt;/strong&gt;-asking the API for new messages at regular intervals-or by receiving a &lt;strong&gt;webhook&lt;/strong&gt;, an HTTP notification SMTPfast sends to our app. Both paths supply metadata and a message ID; the app retrieves the content separately.&lt;/p&gt;

&lt;p&gt;The default local mode polls for message metadata and inserts unseen SMTPfast IDs into PostgreSQL. It needs no public URL. The optional webhook handler verifies the request signature, inserts the same kind of job, and responds with &lt;code&gt;202 Accepted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;worker&lt;/strong&gt; is the background code that processes pending jobs. It retrieves the complete message, finds or creates its ticket, and stores the content the UI can display safely. The support UI reads those stored tickets. When an agent replies, the app calls SMTPfast's reply endpoint for the received message.&lt;/p&gt;

&lt;p&gt;The durable queue separates message discovery from full-message processing and retries. In webhook mode, the handler can acknowledge an event without waiting for the receiving API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure an inbound address
&lt;/h2&gt;

&lt;p&gt;Inbound email is available on SMTPfast's paid plans. You will need a verified sending domain and access to its DNS records. Receiving can use that domain or a subdomain of it in the same SMTPfast team.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;example.com&lt;/code&gt; already uses Google Workspace, Microsoft 365, or another mailbox provider, do not replace its MX records. Use a dedicated subdomain instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inbound.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable receiving for the subdomain in SMTPfast and publish the returned MX record at priority &lt;code&gt;10&lt;/code&gt;. Wait until its receiving status is &lt;code&gt;active&lt;/code&gt;. SMTPfast needs to be the only MX at the lowest-numbered priority. Another MX at the same priority can receive part of the traffic; a lower-numbered record can receive all of it.&lt;/p&gt;

&lt;p&gt;An MX record tells sending mail servers where to deliver email for that domain. You can manage it at your existing DNS provider.&lt;/p&gt;

&lt;p&gt;You can then use &lt;code&gt;support@inbound.example.com&lt;/code&gt; as the app's address. SMTPfast receives the mail; the app turns it into tickets. You do not need to create a Gmail mailbox or configure forwarding for that address.&lt;/p&gt;

&lt;p&gt;Create an API key with only these scopes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inbound:read
email:send
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker needs &lt;code&gt;inbound:read&lt;/code&gt; to retrieve received messages and mint attachment URLs. The UI needs &lt;code&gt;email:send&lt;/code&gt; to reply. It does not need permission to manage domains, contacts, broadcasts, API keys, or received-message deletion.&lt;/p&gt;

&lt;p&gt;That is enough for the local polling quickstart. You do not need a tunnel or webhook for the first run.&lt;/p&gt;

&lt;p&gt;The complete DNS and receiving setup is in SMTPfast's &lt;a href="https://smtpfa.st/docs/inbound" rel="noopener noreferrer"&gt;inbound email documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the TypeScript application
&lt;/h2&gt;

&lt;p&gt;Use Node.js 22 or 24, plus Docker Compose for PostgreSQL. Download or clone the companion repository linked above and open a terminal in its root directory. The snippets below explain the implementation; the repository contains the complete application, schema, templates, and tests.&lt;/p&gt;

&lt;p&gt;Fastify handles the HTTP routes, &lt;code&gt;pg&lt;/code&gt; connects to PostgreSQL, and Zod validates API responses and webhook payloads. The dependency versions are pinned by the repository's lockfile.&lt;/p&gt;

&lt;p&gt;Install the dependencies and copy the example environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgres://tickets:tickets@localhost:55432/tickets
SMTPFAST_BASE_URL=https://smtpfa.st/api/v1
SMTPFAST_API_KEY=sf_live_replace_me
INBOUND_ADDRESS=support@inbound.example.com
INGEST_MODE=poll
APP_SECRET=replace-with-at-least-32-random-characters

ADMIN_USERNAME=admin
ADMIN_PASSWORD=replace-with-a-long-random-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ADMIN_USERNAME&lt;/code&gt; and &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt; are credentials you choose for Mailroom's ticket UI. They are separate from your SMTPfast account and API key. The browser asks for them when you open the inbox. Use a password of at least 12 characters.&lt;/p&gt;

&lt;p&gt;The app uses HTTP Basic authentication to keep this tutorial focused. Plain HTTP is suitable for the localhost demo; a deployed app must use HTTPS to protect those credentials. A multi-user support product should use an identity provider, sessions, roles, and per-ticket authorization.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;APP_SECRET&lt;/code&gt; signs ticket-bound form tokens; it is not an SMTPfast credential. If you later select &lt;code&gt;INGEST_MODE=webhook&lt;/code&gt;, also add &lt;code&gt;SMTPFAST_WEBHOOK_SECRET&lt;/code&gt; from the webhook configuration.&lt;/p&gt;

&lt;p&gt;Generate a value for &lt;code&gt;APP_SECRET&lt;/code&gt; and paste it into &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"console.log(require('node:crypto').randomBytes(32).toString('hex'))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the database, apply the schema, and run the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt; postgres
npm run db:migrate
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; and sign in with your chosen admin credentials. Send an email from your usual mailbox to &lt;code&gt;INBOUND_ADDRESS&lt;/code&gt;. With the default five-second worker interval, it should appear shortly; refresh the inbox to see it. SMTPfast's receiving dashboard is a useful first check if it does not arrive.&lt;/p&gt;

&lt;p&gt;With the app running, a round trip through your usual mailbox makes the flow easier to see. The implementation sections that follow explain what happens at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the complete email conversation
&lt;/h2&gt;

&lt;p&gt;Keep &lt;code&gt;INGEST_MODE=poll&lt;/code&gt; and the app running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a new email with a distinctive subject from Gmail or another external mailbox to &lt;code&gt;INBOUND_ADDRESS&lt;/code&gt;. Refresh Mailroom after a worker cycle and open the new ticket.&lt;/li&gt;
&lt;li&gt;Send a reply from Mailroom. Check the sender's inbox, including spam, for the response.&lt;/li&gt;
&lt;li&gt;Reply to that response from the external mailbox. Refresh Mailroom and confirm the answer appears on the same ticket.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeated worker cycles should leave the message count unchanged. This round trip shows both sides of the integration: incoming mail becomes application data, and an application reply returns to the email conversation.&lt;/p&gt;

&lt;p&gt;If the first message is missing, check SMTPfast's received-mail dashboard. If it is there, confirm that &lt;code&gt;INBOUND_ADDRESS&lt;/code&gt; matches its receiving address and check the app's worker logs. A missing response should be checked in SMTPfast's outbound delivery logs.&lt;/p&gt;

&lt;p&gt;The sections below look inside that flow: how messages become durable work, how replies find their ticket, and how the UI handles untrusted email content. You can use them as a reference when adapting the example to your own application.&lt;/p&gt;

&lt;h2&gt;
  
  
  How polling discovers messages
&lt;/h2&gt;

&lt;p&gt;The receiving list endpoint returns message metadata. The client validates that response and gives the worker the newest items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;listReceivedEmails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReceivedEmailSummary&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boundedLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/emails/receiving?limit=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;boundedLimit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;receivedEmailListSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each worker cycle turns previously unseen received IDs into the same durable jobs that webhook mode creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listReceivedEmails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueueInboundJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`received:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`poll:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email.received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polling is the tutorial default because it works on localhost. The unique event key makes repeated polls harmless. This demo reads only the newest 100 messages across the team, including mail received before the app started. If more than 100 messages arrive between polls, older ones can be missed. A production poller needs pagination and persisted checkpoints; this project's deployment option uses webhooks instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queue notifications without duplicates
&lt;/h2&gt;

&lt;p&gt;Each job needs a stable key so seeing the same notification again does not create more work. This is &lt;strong&gt;idempotency&lt;/strong&gt;: repeating an operation has the same effect as doing it once. Polling uses &lt;code&gt;received:&amp;lt;message ID&amp;gt;&lt;/code&gt;. Webhook mode uses the event ID, with a body hash as a fallback.&lt;/p&gt;

&lt;p&gt;SMTPfast webhook events can carry a provider event ID. The inbound example also permits payloads without that top-level ID, so Mailroom falls back to a SHA-256 hash of the raw body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;webhookEventKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerEventId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;providerEventId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
    &lt;span class="s2"&gt;`sha256:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retries use the same request body, making the hash stable. PostgreSQL enforces uniqueness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;inbound_jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;event_key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;smtpfast_received_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;available_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;completed_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The insert is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;inbound_jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smtpfast_received_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In webhook mode, return &lt;code&gt;202&lt;/code&gt; whether the insert creates a job or finds an existing event key. Mailroom returns &lt;code&gt;503&lt;/code&gt; if it cannot enqueue the event, allowing SMTPfast to retry. Retries are finite, so failed deliveries still need monitoring and replay after the provider exhausts them.&lt;/p&gt;

&lt;p&gt;Do not use an in-memory &lt;code&gt;Set&lt;/code&gt; for this. It forgets everything on a restart and cannot coordinate multiple application instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim durable jobs with row locks
&lt;/h2&gt;

&lt;p&gt;The queue is a PostgreSQL table, so pending work survives an app restart. A worker &lt;strong&gt;claims&lt;/strong&gt; a job by marking it as processing. &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; lets workers claim different rows without waiting for rows another worker is currently locking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;next_jobs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;inbound_jobs&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;available_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;available_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inbound_jobs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;next_jobs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second condition recovers jobs left behind by a crashed process. After five minutes, another worker can claim them again.&lt;/p&gt;

&lt;p&gt;This five-minute lease is not an exactly-once guarantee. A job that outlives its lease can be reclaimed; the unique &lt;code&gt;messages.smtpfast_received_id&lt;/code&gt; constraint is the final guard against storing the same received message twice. Row locks also do not guarantee that two different replies in one conversation finish in order. If your workflow depends on that ordering, serialize processing per conversation.&lt;/p&gt;

&lt;p&gt;Failed jobs use exponential backoff capped at five minutes, with eight processing attempts by default. Non-retryable API responses, such as a permanent &lt;code&gt;404&lt;/code&gt;, go directly to &lt;code&gt;failed&lt;/code&gt;. A production deployment should alert on those rows and expose a dead-letter review workflow.&lt;/p&gt;

&lt;p&gt;The web server and worker run in one process for this example. They can be separated when they need independent scaling; queue age and failed-job counts help reveal when processing is falling behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetch the full message in the worker
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;email.received&lt;/code&gt; event contains metadata, not the bodies. The worker retrieves the message by its SMTPfast ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/emails/receiving/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;receivedId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full object includes plain text, HTML, headers, attachment metadata, the SMTP envelope, authentication and scanning verdicts, and threading fields.&lt;/p&gt;

&lt;p&gt;An inbound domain is effectively catch-all: every local address can receive. Mailroom should create tickets only for one configured mailbox. It checks the envelope recipient rather than the visible header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envelopeRecipients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received_for&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;received_for&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;envelope_to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intendedForMailbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;envelopeRecipients&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inboundAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;intendedForMailbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markJobIgnored&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Envelope recipient did not match &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inboundAddress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why not trust &lt;code&gt;To&lt;/code&gt;? A message can be Bcc'd, forwarded, or carry a forged display header. SMTP routing happens against the envelope, and SMTPfast exposes the actual recipient through &lt;code&gt;received_for&lt;/code&gt; and &lt;code&gt;envelope_to&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn email headers into conversations
&lt;/h2&gt;

&lt;p&gt;The first message creates a ticket. Later replies should attach to it.&lt;/p&gt;

&lt;p&gt;Email clients express conversation relationships using three headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Message-ID&lt;/code&gt; uniquely identifies a message.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;In-Reply-To&lt;/code&gt; normally points at the message being answered.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;References&lt;/code&gt; carries the earlier IDs in the conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mailroom stores the incoming &lt;code&gt;Message-ID&lt;/code&gt;. For a new message, it searches both &lt;code&gt;In-Reply-To&lt;/code&gt; and &lt;code&gt;References&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ticket_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;internet_message_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some clients or forwarding systems strip threading headers. Replies sent by Mailroom therefore include a ticket marker in the subject:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Re: [Ticket #42] CSV export is missing last week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subject marker is a fallback, not the primary key. Header-based matching runs first because users can type or copy subject text themselves. Mailroom accepts the fallback only when the normalized sender address matches the ticket requester; otherwise it creates a new ticket. That is a routing safeguard, not authentication-the &lt;code&gt;From&lt;/code&gt; header is still untrusted and no email should authorize a sensitive account action by itself.&lt;/p&gt;

&lt;p&gt;The final database transaction performs all of these operations together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check whether the received SMTPfast message ID already exists.&lt;/li&gt;
&lt;li&gt;Resolve a ticket from threading headers or the subject marker.&lt;/li&gt;
&lt;li&gt;Create a ticket when neither produces a match.&lt;/li&gt;
&lt;li&gt;Insert the message and attachment metadata.&lt;/li&gt;
&lt;li&gt;Mark the queue job complete.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the process stops before commit, none of those changes become visible and the stale job can be reclaimed safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat email content as hostile input
&lt;/h2&gt;

&lt;p&gt;The easiest way to create an XSS problem is to place &lt;code&gt;message.html&lt;/code&gt; inside the support UI.&lt;/p&gt;

&lt;p&gt;Email HTML can contain tracking pixels, deceptive links, CSS designed to hide content, and markup that was never intended to share a document with your application. Mailroom takes the conservative route: it stores and displays the plain-text body, and every value entering an HTML template is escaped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;escapeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#039;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI also ships a restrictive Content Security Policy and does not load remote images.&lt;/p&gt;

&lt;p&gt;SMTPfast supplies SPF, DKIM, DMARC, spam, and virus verdicts. These are useful context, not a substitute for application authorization. A message passing SPF proves something about its sending path; it does not mean the person is allowed to update an order or access an account.&lt;/p&gt;

&lt;p&gt;When the virus verdict does not pass, SMTPfast quarantines the message and does not expose its bodies, raw file, or attachments. Mailroom stores only the available metadata and flags the message as quarantined. A new ticket created from that message also receives the &lt;code&gt;quarantined&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;An HTML-only message without a plain-text part appears without body text in this demo. Adding an HTML-to-text conversion step would improve that case without rendering the original HTML.&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%2Fy0nort9ayg892mo8u7jl.jpg" 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%2Fy0nort9ayg892mo8u7jl.jpg" alt="Mailroom showing a sample ticket with three messages, scanning verdicts, attachment metadata, and a reply form" width="800" height="890"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sample conversation from the demo seed data.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep attachment access short-lived
&lt;/h2&gt;

&lt;p&gt;The message object contains attachment metadata, but download URLs are minted on request and expire after 15 minutes. Mailroom stores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMTPfast attachment ID
filename
declared content type
size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not save the temporary URL. When an authenticated agent clicks an attachment, the app asks SMTPfast for a fresh URL and redirects only if the result uses HTTPS.&lt;/p&gt;

&lt;p&gt;This keeps expired credentials out of the database, but it does not make the file safe. Filenames and MIME types are attacker-controlled. A production workflow that redistributes attachments should scan them again, enforce size and type policy based on file contents, and preferably use content-disarm tooling for risky document formats.&lt;/p&gt;

&lt;p&gt;SMTPfast currently retains received messages and attachments for up to 30 days. If tickets must preserve files longer, copy approved attachments into storage you control and define a lifecycle policy there.&lt;/p&gt;

&lt;p&gt;That provider retention window does not remove the message text already copied into PostgreSQL. Your application needs its own retention and deletion policy for stored conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reply without breaking the email thread
&lt;/h2&gt;

&lt;p&gt;To answer a ticket, Mailroom finds its latest inbound message. If that message is quarantined, it blocks the reply; otherwise it calls the reply endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/emails/receiving/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;receivedId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/reply`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Re: [Ticket #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;replyText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SMTPfast adds &lt;code&gt;In-Reply-To&lt;/code&gt; and &lt;code&gt;References&lt;/code&gt; for the received conversation. These let the recipient's email client associate the reply with the thread, although the client ultimately controls its display. The next answer carries references we can match back to the ticket.&lt;/p&gt;

&lt;p&gt;SMTPfast chooses the sending address from the receiving domain or its verified parent. For a receive-only subdomain, it sets &lt;code&gt;Reply-To&lt;/code&gt; back to the inbound address. It also chooses the recipient from the original message's &lt;code&gt;Reply-To&lt;/code&gt;, falling back to &lt;code&gt;From&lt;/code&gt;; those fields remain untrusted email input. See the &lt;a href="https://smtpfa.st/docs/inbound#replying-from-the-mailbox" rel="noopener noreferrer"&gt;reply endpoint's address rules&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After SMTPfast accepts the reply, Mailroom stores the returned outbound email ID beside the local message. That correlation ID is the bridge to delivery logs and later delivery events.&lt;/p&gt;

&lt;p&gt;Acceptance is not proof of delivery. Also, sending and recording the reply are separate operations: a crash between them could leave a sent reply missing locally, and submitting the form again can send another copy. A production version needs an outbound outbox, reconciliation, and provider-supported idempotency where available.&lt;/p&gt;

&lt;p&gt;The reply form is protected by Basic authentication and a ticket-bound HMAC form token. The token is not a replacement for user sessions, but it prevents an unrelated site from constructing a valid reply form submission.&lt;/p&gt;

&lt;p&gt;For a shared support inbox, this is also where user roles, ticket ownership, state changes, and an audit trail become useful. Those features belong to the application using the email pipeline and can follow its existing authentication and authorization model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: switch to signed webhooks
&lt;/h2&gt;

&lt;p&gt;You can keep polling for the entire local tutorial. To use webhooks after deploying behind public HTTPS, create a &lt;strong&gt;standard&lt;/strong&gt; webhook subscribed to &lt;code&gt;email.received&lt;/code&gt;, with this endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-app.example/webhooks/smtpfast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy its signing secret into &lt;code&gt;.env&lt;/code&gt;, change the ingestion mode, and restart the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INGEST_MODE=webhook
SMTPFAST_WEBHOOK_SECRET=whsec_replace_me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once configured, replay a delivery from SMTPfast's webhook log and confirm it does not add a second ticket or message. The queue's &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;last_error&lt;/code&gt; fields help distinguish ignored recipients, retries, and failed jobs.&lt;/p&gt;

&lt;p&gt;The signing secret verifies requests arriving at your app. The API key authorizes requests your app sends to SMTPfast; keep them separate.&lt;/p&gt;

&lt;p&gt;SMTPfast retries unsuccessful webhook deliveries after 1 second, 30 seconds, 5 minutes, and 30 minutes. Keeping the handler short helps avoid timeouts; the &lt;a href="https://smtpfa.st/docs/webhooks" rel="noopener noreferrer"&gt;webhook documentation&lt;/a&gt; describes the complete policy.&lt;/p&gt;

&lt;p&gt;Standard SMTPfast webhooks include &lt;code&gt;X-SMTPfast-Signature&lt;/code&gt;, an HMAC-SHA256 digest of the raw request body.&lt;/p&gt;

&lt;p&gt;The word &lt;strong&gt;raw&lt;/strong&gt; matters. If a framework parses JSON and later serializes it again, whitespace or property order may change. The content still means the same thing, but the bytes no longer match the signed bytes.&lt;/p&gt;

&lt;p&gt;Mailroom tells Fastify to keep JSON bodies as a &lt;code&gt;Buffer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeContentTypeParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addContentTypeParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;parseAs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;buffer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signature verification uses buffers and a constant-time comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-f&lt;/span&gt;&lt;span class="se"&gt;\d]{64}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supplied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;supplied&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supplied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after verification do we call &lt;code&gt;JSON.parse&lt;/code&gt; and validate the shape with Zod. This excerpt shows that ordering; the full handler also catches malformed JSON, returns &lt;code&gt;400&lt;/code&gt; for invalid payloads, and enqueues the validated event before returning &lt;code&gt;202&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/smtpfast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;415&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expected application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-smtpfast-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpfastWebhookSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid webhook signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email.received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;smtpfastWebhookEventSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Enqueue next...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid signature establishes that the bytes were signed with the shared secret and have not changed. It does not establish freshness or make the email's contents trustworthy. The sender, subject, body, filenames, MIME types, and headers remain untrusted input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure boundaries
&lt;/h2&gt;

&lt;p&gt;The automated tests do not send real email. The fast suite uses test doubles for the provider client and persistence layer; the integration suite uses a real PostgreSQL database.&lt;/p&gt;

&lt;p&gt;The fast tests cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correct and forged webhook signatures&lt;/li&gt;
&lt;li&gt;Polling discovery and duplicate suppression&lt;/li&gt;
&lt;li&gt;The fact that changing raw JSON whitespace invalidates the signature&lt;/li&gt;
&lt;li&gt;Duplicate webhook acknowledgement&lt;/li&gt;
&lt;li&gt;Rejection of unauthenticated ticket access&lt;/li&gt;
&lt;li&gt;Envelope-based mailbox routing&lt;/li&gt;
&lt;li&gt;Ignoring other addresses on the catch-all domain&lt;/li&gt;
&lt;li&gt;Retryable and permanent provider failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PostgreSQL integration tests exercise the real schema. One creates two received messages whose &lt;code&gt;References&lt;/code&gt; headers point to the same conversation and asserts that both land on one ticket. Another supplies a quarantined body and attachment, then asserts that neither was stored.&lt;/p&gt;

&lt;p&gt;The third checks that a different sender cannot join a ticket merely by copying its subject marker.&lt;/p&gt;

&lt;p&gt;Run the fast suite with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three database tests are skipped unless &lt;code&gt;TEST_DATABASE_URL&lt;/code&gt; is set. &lt;strong&gt;Use a dedicated test database: the integration suite truncates its tables before each test.&lt;/strong&gt; Create it once and apply the schema before running the suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;postgres createdb &lt;span class="nt"&gt;-U&lt;/span&gt; tickets tickets_test

&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://tickets:tickets@localhost:55432/tickets_test &lt;span class="se"&gt;\&lt;/span&gt;
  npm run db:migrate

&lt;span class="nv"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://tickets:tickets@localhost:55432/tickets_test &lt;span class="se"&gt;\&lt;/span&gt;
  npm run &lt;span class="nb"&gt;test&lt;/span&gt;:integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Signature tests should use the exact &lt;code&gt;Buffer&lt;/code&gt; sent to the handler. Testing only the HMAC helper without the framework parser can miss the most common integration mistake.&lt;/p&gt;

&lt;p&gt;For more detail on the provider API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://smtpfa.st/docs/inbound" rel="noopener noreferrer"&gt;SMTPfast inbound email API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://smtpfa.st/docs/webhooks" rel="noopener noreferrer"&gt;SMTPfast webhook signing and retries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://smtpfa.st/docs/api-keys" rel="noopener noreferrer"&gt;SMTPfast API-key scopes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Email can be a useful interface to an application, especially when people already use it to ask for help, share documents, or continue a conversation. Mailroom shows how that familiar interaction can become structured data: an incoming message creates a ticket, later replies join it, and an agent's response travels back through email.&lt;/p&gt;

&lt;p&gt;SMTPfast handles receiving and parsing the message. The application decides what it means, how it connects to existing records, and who can act on it. Keeping discovery, background processing, and display separate makes those decisions easier to understand and adapt. Polling lets you explore the flow locally; signed webhooks offer another way to discover messages once the app is deployed.&lt;/p&gt;

&lt;p&gt;To use the pattern elsewhere, start with the point where Mailroom creates a ticket. That could instead create a bug report, register a document for review, or attach a reply to an existing discussion. Try a conversation in the example app, follow it through the code, and use the pieces that fit your workflow.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The 12 Best DevOps Newsletters to Subscribe to in 2026</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:49:38 +0000</pubDate>
      <link>https://dev.to/boiliev/the-12-best-devops-newsletters-to-subscribe-to-in-2026-n97</link>
      <guid>https://dev.to/boiliev/the-12-best-devops-newsletters-to-subscribe-to-in-2026-n97</guid>
      <description>&lt;p&gt;I used to subscribe to everything. Then my inbox became a second job, and I started unsubscribing from anything I had not opened in a month. These are the newsletters that survived.&lt;/p&gt;

&lt;p&gt;Before writing this, I checked that every one of them is still publishing as of October 2026. That matters more than it sounds: two of the most recommended DevOps newsletters stopped a while ago, and most "best of" lists still send people to them. More on those at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. DevOps Daily
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://devops-daily.com/newsletters" rel="noopener noreferrer"&gt;devops-daily.com/newsletters&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, every Monday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the one I open first. Each issue is a short roundup of what went up on DevOps Daily that week, and what makes it different is the type of content. The posts are hands-on: they run the thing, record the output and publish the numbers, so you get "we upgraded the same OpenTofu state and here is what broke" instead of a summary of release notes.&lt;/p&gt;

&lt;p&gt;It also links to the site's interactive material: terminal simulators you run in the browser (Docker, Kubernetes, Git, Linux and more), quizzes, flashcards, interview questions and roadmaps. If you learn best by doing, this is the best fit on the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; engineers from junior to senior who want practical posts they can try the same day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One caveat:&lt;/strong&gt; it covers the DevOps Daily site, not the whole industry. Pair it with one of the news roundups below if you also want the week's headlines.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SRE Weekly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://sreweekly.com" rel="noopener noreferrer"&gt;sreweekly.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lex Neva has run this for more than 500 issues. It collects incident write-ups, postmortems and articles on reliability and on-call culture, each with a short note on why it is worth your time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; SREs and anyone who carries a pager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; it is mostly about incidents and culture, with little tooling news.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. DevOps'ish
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://devopsish.com" rel="noopener noreferrer"&gt;devopsish.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Sunday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chris Short's take on cloud native, open source and the industry around them. It has opinions, and that is the point: you get context, not only links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; people who want the week's cloud native news with commentary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; issues are long, and a fair part is general tech news rather than DevOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. TLDR DevOps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://tldr.tech/devops" rel="noopener noreferrer"&gt;tldr.tech/devops&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; three issues a week (Monday, Wednesday, Friday)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Short summaries of tools, releases and articles. You can read an issue in two minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; people who want to scan headlines quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; the summaries are short, so you click through for anything that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Learn Kubernetes Weekly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://kube.today/learn-kubernetes-weekly" rel="noopener noreferrer"&gt;kube.today/learn-kubernetes-weekly&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Wednesday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A curated set of Kubernetes articles, tutorials and tools. It started at learnk8s.io and now lives at kube.today (the old link redirects).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; anyone who runs or is learning Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; Kubernetes only.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Terraform Weekly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://weekly.tf" rel="noopener noreferrer"&gt;weekly.tf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Wednesday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anton Babenko, who maintains some of the most used Terraform AWS modules, picks the week's Terraform and OpenTofu news, modules and articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; anyone who writes infrastructure as code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; issues are very short, a one to three minute read.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Platform Weekly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://platformweekly.com" rel="noopener noreferrer"&gt;platformweekly.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Friday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Essays and links on platform engineering and developer experience, from Luca Galante and the platformengineering.org community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; people who build internal platforms or lead a platform team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; it is more essays than a link roundup, and it is close to one community.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. DevOps Bulletin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://devopsbulletin.com" rel="noopener noreferrer"&gt;devopsbulletin.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Friday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mohamed Labouardy's roundup of DevOps, cloud cost and security links, plus a few open source tools each week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; people who want one broad weekly digest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; lots of links, not much commentary.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Last Week in AWS
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://www.lastweekinaws.com/newsletter/" rel="noopener noreferrer"&gt;lastweekinaws.com/newsletter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Monday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Corey Quinn reads the AWS announcements so you do not have to, and tells you which ones matter, with a lot of snark.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; anyone with a real AWS bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; AWS only, and some weeks are skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Console.dev
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://console.dev" rel="noopener noreferrer"&gt;console.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Thursday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Short, honest reviews of new developer tools and betas. Each tool gets a "what we like" and "what we don't like".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; people who like trying new tools before everyone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; general developer tools, not only DevOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. CloudSecList
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://cloudseclist.com" rel="noopener noreferrer"&gt;cloudseclist.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Sunday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marco Lancini's weekly list of cloud security research, tools and incidents, with a growing section on AI security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; DevOps and platform engineers who own security in the cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; cloud security only, on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. tl;dr sec
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://tldrsec.com" rel="noopener noreferrer"&gt;tldrsec.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cadence:&lt;/strong&gt; weekly, on Thursday&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clint Gibler's security newsletter, known for long, well-organized issues on application and cloud security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it suits:&lt;/strong&gt; anyone who wants to keep up with security without reading 50 blogs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; security first, so only part of each issue is DevOps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Last Week in Kubernetes Development
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href="https://lwkd.info" rel="noopener noreferrer"&gt;lwkd.info&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you follow Kubernetes upstream, this summarizes the week's merges, deprecations and release dates. It is for contributors and people who plan upgrades, not for ecosystem news.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two classics that stopped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DevOps Weekly:&lt;/strong&gt; the last issue is #700 from June 2024, where the author said he was taking a break. The sign-up form still works, which is why people keep recommending it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KubeWeekly:&lt;/strong&gt; the CNCF ended it with issue #434 in May 2025 and moved it into a monthly CNCF newsletter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I keep up without drowning
&lt;/h2&gt;

&lt;p&gt;Do not subscribe to all twelve. Pick three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One that teaches you something hands-on:&lt;/strong&gt; DevOps Daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One for your main stack:&lt;/strong&gt; Learn Kubernetes Weekly, Terraform Weekly or Last Week in AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One for news or security:&lt;/strong&gt; DevOps'ish or TLDR DevOps for news, CloudSecList or tl;dr sec for security.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is about 30 minutes of reading a week, and you will not miss much that matters.&lt;/p&gt;

&lt;p&gt;Which newsletter did I miss? Tell me in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>newsletter</category>
      <category>sre</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Your App Works. Your Emails Don’t. 7 Services Worth Trying in 2026</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Mon, 05 Oct 2026 20:10:10 +0000</pubDate>
      <link>https://dev.to/boiliev/your-app-works-your-emails-dont-7-services-worth-trying-in-2026-2cpj</link>
      <guid>https://dev.to/boiliev/your-app-works-your-emails-dont-7-services-worth-trying-in-2026-2cpj</guid>
      <description>&lt;p&gt;Your deployment is green. Signups are coming in. The password reset endpoint returns a successful response.&lt;/p&gt;

&lt;p&gt;Then a user messages support:&lt;/p&gt;

&lt;p&gt;“I never got the email.”&lt;/p&gt;

&lt;p&gt;You check the application logs. No exceptions. The request went through. As far as your backend is concerned, everything worked.&lt;/p&gt;

&lt;p&gt;For the person locked out of their account, it didn’t.&lt;/p&gt;

&lt;p&gt;Email sits in that awkward space between application code and infrastructure you don’t fully control. Sending a message is one step. Knowing what happened afterward is where the debugging starts.&lt;/p&gt;

&lt;p&gt;If you’re building a SaaS product, side project, or customer-facing application, these seven services deserve a place on your shortlist.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Editorial note: This article highlights SMTPFast. It compares documented features rather than presenting a hands-on performance ranking.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SMTPFast - For sending email and tracing what happened next
&lt;/h2&gt;

&lt;p&gt;When a user reports a missing email, you need more than a successful API response. You need a way to investigate that specific message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://smtpfa.st/" rel="noopener noreferrer"&gt;SMTPFast&lt;/a&gt; supports both a REST API and an SMTP bridge. Its email API lets you retrieve an individual message’s status and delivery events, while SMTP submissions use the same queueing, logs, suppression checks, and webhooks. That makes it worth considering when your stack includes both new API integrations and applications that already speak SMTP. &lt;a href="https://smtpfa.st/docs/emails" rel="noopener noreferrer"&gt;Email API documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a SaaS app, the practical use cases are familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Welcome and account verification emails.&lt;/li&gt;
&lt;li&gt;Password resets and login messages.&lt;/li&gt;
&lt;li&gt;Receipts and billing notifications.&lt;/li&gt;
&lt;li&gt;Alerts triggered by activity inside your product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration starts with verifying your sending domain and creating an API key. SMTPFast also documents compatibility with Resend, which gives teams using that API a migration path to evaluate. Check the supported behavior against your existing integration before switching. &lt;a href="https://smtpfa.st/docs" rel="noopener noreferrer"&gt;Quick-start documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; You want API and SMTP sending, plus visibility into individual email events.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Resend - For developers who build email alongside their application
&lt;/h2&gt;

&lt;p&gt;Email templates can become their own maintenance problem. Your application uses components, but your emails live in a separate collection of HTML files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; is worth a look if you want a developer-oriented workflow, particularly around React Email. It provides an email API, SMTP support, and testing tools for experimenting with sending behavior. &lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend features&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The appeal is how email fits into your development process: building templates, integrating sends, and testing changes before users encounter them.&lt;/p&gt;

&lt;p&gt;For a React-heavy team, that may be an easier starting point than adopting an email workflow built primarily around a marketing dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; Your team wants to manage email templates and integrations alongside application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Postmark - For applications where transactional email is central
&lt;/h2&gt;

&lt;p&gt;Some emails are part of the product’s critical path.&lt;/p&gt;

&lt;p&gt;A delayed newsletter is inconvenient. A missing password reset can stop someone from using your application altogether.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://postmarkapp.com/" rel="noopener noreferrer"&gt;Postmark&lt;/a&gt; offers API and SMTP sending, templates, inbound email, analytics, and webhooks. One notable feature is Message Streams: it routes transactional and broadcast messages through separate sending infrastructure. &lt;a href="https://www.postmarkapp.com/" rel="noopener noreferrer"&gt;Postmark features&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That separation is relevant when the same business sends both account-related messages and newsletters. It gives you an explicit way to organize those different sending workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; Account notifications, receipts, and other transactional messages are a core part of your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Mailgun - For teams that want email events feeding back into their app
&lt;/h2&gt;

&lt;p&gt;An email bounce should often trigger an application action.&lt;/p&gt;

&lt;p&gt;Perhaps you need to flag an address, stop further sends, or prompt the user to update their contact details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mailgun.com/" rel="noopener noreferrer"&gt;Mailgun&lt;/a&gt; provides email tooling with logs and webhooks. Its webhooks send event data to your endpoints, allowing your application to respond when something happens to a message. &lt;a href="https://www.mailgun.com/integrations/webhooks/" rel="noopener noreferrer"&gt;Webhook documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That makes it a useful candidate when email is part of a larger backend workflow. Rather than relying entirely on dashboard checks, you can build handling around the events your application receives.&lt;/p&gt;

&lt;p&gt;The integration work includes deciding how to process those events and what each one should change in your system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; You want email delivery events to drive backend behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Amazon SES - For teams comfortable owning more of the setup
&lt;/h2&gt;

&lt;p&gt;If your application already runs on AWS, &lt;a href="https://aws.amazon.com/ses/" rel="noopener noreferrer"&gt;Amazon Simple Email Service&lt;/a&gt; belongs on the shortlist.&lt;/p&gt;

&lt;p&gt;SES provides application email sending with usage-based pricing. For teams evaluating larger sending volumes, that pricing model is worth comparing against subscription plans. &lt;a href="https://aws.amazon.com/ses/" rel="noopener noreferrer"&gt;SES overview&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The operational setup deserves attention, though. New SES accounts start in a sandbox, with restrictions that include sending only to verified recipients or the mailbox simulator. Sending to other recipients requires production access in the relevant AWS Region. &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html" rel="noopener noreferrer"&gt;Sandbox and production access documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consider the engineering time alongside the email bill: account configuration, monitoring, and event handling are part of the decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; Your team knows AWS and wants more control over its email infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. SendGrid - For teams evaluating API, SMTP, and event reporting
&lt;/h2&gt;

&lt;p&gt;Your email integration may need to serve several applications, frameworks, or existing systems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sendgrid.com/" rel="noopener noreferrer"&gt;Twilio SendGrid&lt;/a&gt; supports SMTP integration and offers an Event Webhook for reporting what happens to messages. Its documented events include processing, delivery, bounces, and engagement events. &lt;a href="https://www.twilio.com/docs/sendgrid/for-developers/sending-email/integrating-with-the-smtp-api" rel="noopener noreferrer"&gt;SMTP documentation&lt;/a&gt;, &lt;a href="https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event" rel="noopener noreferrer"&gt;Event Webhook reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When evaluating it, map your requirements to the plan you’re considering. Sending volume is one dimension; access to the reporting and operational features you need matters too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; You’re comparing established services for multiple email integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Brevo - For businesses that also need customer communication tools
&lt;/h2&gt;

&lt;p&gt;Sometimes the email decision extends beyond the backend team.&lt;/p&gt;

&lt;p&gt;Developers need password resets and receipts. Other people in the business need campaigns and customer communication workflows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.brevo.com/" rel="noopener noreferrer"&gt;Brevo&lt;/a&gt; is worth considering in that situation. Its transactional email service supports sending through an API or SMTP, including messages triggered by actions inside an application. &lt;a href="https://help.brevo.com/hc/en-us/articles/7924148470546-How-can-I-send-transactional-emails-with-Brevo" rel="noopener noreferrer"&gt;Transactional email documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Evaluate it with both groups in mind. A platform that suits your application should also fit the way your team manages templates and customer messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth trying if:&lt;/strong&gt; Your business wants to evaluate transactional email within a broader communication platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose without turning it into a month-long project
&lt;/h2&gt;

&lt;p&gt;Start with one real application flow: a password reset, a welcome message, or a receipt.&lt;/p&gt;

&lt;p&gt;Then evaluate each shortlisted service against the same questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much work does integration require?&lt;/li&gt;
&lt;li&gt;Can you locate a specific message and understand its status?&lt;/li&gt;
&lt;li&gt;Can your application react to delivery failures?&lt;/li&gt;
&lt;li&gt;What limits and retention periods apply to your plan?&lt;/li&gt;
&lt;li&gt;What will it cost at your expected volume?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the same test messages and sending domain for a more useful comparison. Include a failure case, rather than testing only the happy path.&lt;/p&gt;

&lt;p&gt;If SMTPFast’s combination of API sending, SMTP support, and message-level tracing matches your needs, start with its &lt;a href="https://smtpfa.st/docs" rel="noopener noreferrer"&gt;quick-start guide&lt;/a&gt; and connect one application flow.&lt;/p&gt;

&lt;p&gt;The next time someone says “I never got the email,” you’ll have a concrete place to start investigating.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>sass</category>
      <category>email</category>
    </item>
    <item>
      <title>Does Your AI Agent Have a Kill Switch?</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/atomsized/does-your-ai-agent-have-a-kill-switch-396j</link>
      <guid>https://dev.to/atomsized/does-your-ai-agent-have-a-kill-switch-396j</guid>
      <description>&lt;p&gt;&lt;strong&gt;The agent believed it was working inside a simulation. The systems it reached were real. Anthropic's report shows why a prompt cannot carry the whole safety boundary for an autonomous task.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An engineer should know what the agent is doing, what it can reach and how to stop it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On 30 July 2026, Anthropic published a review of 141,006 cybersecurity evaluation runs. It found three incidents, across six runs, where Claude reached the internet and gained unauthorised access to real organisations.&lt;/p&gt;

&lt;p&gt;The models were running capture-the-flag exercises. Their prompts said the environment was simulated and had no internet access. A misunderstanding with the evaluation partner left live internet access available, so the models often treated real systems as part of the exercise.&lt;/p&gt;

&lt;p&gt;The models followed their task with a false picture of the environment. The runtime allowed actions that the prompt said were impossible, and the activity was found later through transcript review.&lt;/p&gt;

&lt;p&gt;An engineer should understand the task before it begins, see its progress and be able to stop it immediately. The platform must enforce those controls even when the model misunderstands the situation.&lt;/p&gt;

&lt;h2&gt;The agent had the wrong picture of its environment.&lt;/h2&gt;

&lt;p&gt;Anthropic's evaluation prompt told Claude that it had no internet access. The machines used for the exercise could reach the internet because of a misconfiguration. The agent trusted the description in the prompt and treated reachable systems as part of the simulated challenge.&lt;/p&gt;

&lt;p&gt;The incidents had real effects. One reached production infrastructure and a database. Another published a malicious Python package to PyPI. It was available for roughly one hour and ran on 15 real systems. A third scanned thousands of targets before compromising an internet-facing application.&lt;/p&gt;

&lt;p&gt;The models reacted differently to signs that the targets were real. An older model continued. Another convinced itself that it was still in a simulation. Anthropic's latest research model stopped after recognising the real environment.&lt;/p&gt;

&lt;p&gt;Anthropic describes this as closer to a harness and operational failure than a model alignment failure. Normal production safeguards were absent because the evaluations measured the underlying models. Containment and monitoring therefore carried more responsibility.&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;The prompt described the intended world. The infrastructure exposed the real one.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Make the environment prove what the agent can reach.&lt;/h2&gt;

&lt;p&gt;A prompt can name the approved environment, resources and goal. It cannot close a network route, remove an IAM permission or prevent a tool from calling an unapproved endpoint.&lt;/p&gt;

&lt;p&gt;Build the smallest working environment for the task. Give the agent a separate identity, short-lived credentials, an allowlist of tools and resources, a network policy and a time limit. Check those controls from inside the runtime it will use.&lt;/p&gt;

&lt;p&gt;Name the account, cluster, repository, namespace, service and environment. Describe which systems are real, simulated or outside scope. If the runtime disagrees with that description, stop before the first action.&lt;/p&gt;

&lt;p&gt;Review third-party execution environments as well. The team needs to know who owns the network, identity, logs, emergency stop and incident response. An assumption between two organisations can become a live access path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify outbound network access from inside the agent runtime.&lt;/li&gt;
&lt;li&gt;Issue one short-lived identity for one task and one environment.&lt;/li&gt;
&lt;li&gt;Allow only the tools, resources and actions required for the approved goal.&lt;/li&gt;
&lt;li&gt;Record the expected account, cluster, repository and service before execution.&lt;/li&gt;
&lt;li&gt;Stop when the observed environment differs from the approved context.&lt;/li&gt;
&lt;li&gt;Confirm who owns monitoring and incident response across every vendor boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;Controls should still hold when the agent's understanding is wrong.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Give the engineer a live view of the task.&lt;/h2&gt;

&lt;p&gt;Before the run, the responsible engineer should see the goal, plan, environment, identity, tools, access boundary, time limit and stop conditions. A broad instruction such as investigate the cluster leaves too much room for interpretation.&lt;/p&gt;

&lt;p&gt;During the run, show the current step, tool call, target resource, network destination, command, proposed change and evidence. Highlight denied requests, repeated attempts, new destinations and actions outside the plan.&lt;/p&gt;

&lt;p&gt;Keep the important state above the raw transcript: the goal, current action, completed changes, remaining access and failed policy checks. Preserve the full trace underneath for audit.&lt;/p&gt;

&lt;p&gt;Alert the named owner when the agent approaches a boundary. The owner can follow normal reads through the activity view and receive a clear signal when approval is required or the task changes shape.&lt;/p&gt;

&lt;h3&gt;Keep the plan, live work and stop path together&lt;/h3&gt;

&lt;p&gt;The engineer should be able to understand and control the task from one place.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Approved goal:&lt;/strong&gt; Name the owner, environment, result and time limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded identity:&lt;/strong&gt; Issue short-lived access for the approved tools and resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live activity:&lt;/strong&gt; Show actions, targets, evidence, retries and denied requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy checks:&lt;/strong&gt; Compare every action with the approved scope before the tool runs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineer checkpoint:&lt;/strong&gt; Pause before a production change, new destination or high-risk action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External stop:&lt;/strong&gt; End the task and remove its access through a control the agent cannot change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserved evidence:&lt;/strong&gt; Keep the plan, tool calls, logs, diffs and reason for stopping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verified recovery:&lt;/strong&gt; Check the desired state and let the engineer decide the next step.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;&lt;p&gt;Awareness means seeing the plan, the current action and the remaining authority.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;A kill switch must stop access as well as reasoning.&lt;/h2&gt;

&lt;p&gt;Industries that use machinery design an emergency stop into the system. An infrastructure agent can cross networks, use credentials and change real resources quickly. Design its stop before it starts.&lt;/p&gt;

&lt;p&gt;Ending the model response leaves other work running. Tool calls, CI jobs, cloud operations, retries and scheduled tasks may continue after the visible conversation ends.&lt;/p&gt;

&lt;p&gt;Place the kill switch in the orchestrator or a separate control service. The agent cannot disable it, edit its policy or issue itself a new identity. Engineers and automatic policy controls should both be able to trigger it.&lt;/p&gt;

&lt;p&gt;Stopping also needs a recovery choice. Preserve the evidence, compare the running system with the approved state and let the engineer choose whether to reconcile, revert or investigate further.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop the active run:&lt;/strong&gt; End new calls, then terminate active sessions and jobs where possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove authority:&lt;/strong&gt; Revoke the task identity, expire tokens and block its network and tool access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancel future work:&lt;/strong&gt; Clear queued jobs, retries, schedules and child tasks created by the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve the record:&lt;/strong&gt; Store the plan, prompts, actions, destinations, responses, diffs, logs and stop reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the real state:&lt;/strong&gt; Compare Git, GitOps, Kubernetes and cloud state with the last approved result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return ownership:&lt;/strong&gt; Give the responsible engineer the evidence and recovery choices before any restart.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;The stop control must remain available when the agent, model or runtime behaves unexpectedly.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Keep engineers close to actions that change the system.&lt;/h2&gt;

&lt;p&gt;Engineers should see the plan, the live activity and every point where approval is needed. Routine reads can continue inside the approved boundary.&lt;/p&gt;

&lt;p&gt;Read-only evidence gathering can publish a report for review. Infrastructure changes should arrive as a diff or pull request. Git records the proposal, CI checks it and an engineer approves it before GitOps applies the accepted state.&lt;/p&gt;

&lt;p&gt;Identity, networking, data, deletion and security changes deserve explicit approval. Small reversible actions may gain more automation after the team tests the limits, recovery and stop path with real runs.&lt;/p&gt;

&lt;p&gt;For Atomsized workflows, AI reads and prepares. Engineers decide and approve. Git records the accepted change. GitOps applies it. AWS and monitoring report the result.&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;The engineer owns the decision, the boundary and the recovery.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Begin with one task and rehearse the stop.&lt;/h2&gt;

&lt;p&gt;Choose one repeated task with a known owner and clear result. Read-only investigation lets the team compare the agent's work with an engineer's process before giving it change access.&lt;/p&gt;

&lt;p&gt;Write down the scope and pause conditions. Build the live view and kill switch, then replay earlier incidents where the correct outcome was to ask for help or stop.&lt;/p&gt;

&lt;p&gt;Run beside an engineer. Review the actions, access, weak context and alerts. Expand only after the team trusts the monitoring and has rehearsed recovery.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1. Choose the task:&lt;/strong&gt; Name one owner, one environment, one expected result and one maximum runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2. Map every action:&lt;/strong&gt; List the tools, resources, destinations, permissions and data the task may use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3. Build the boundary:&lt;/strong&gt; Create the task identity, allowlists, network rules and automatic stop conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4. Show the live state:&lt;/strong&gt; Put the plan, current action, evidence, changes and policy decisions in one view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5. Test the kill switch:&lt;/strong&gt; Stop an active tool call, revoke access, cancel queued work and confirm the evidence remains available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6. Run in shadow mode:&lt;/strong&gt; Use real work with no change authority, compare results and fix gaps before adding another action.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;A team should prove that it can stop the agent before asking the agent to do more.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Test what happens after the button is pressed.&lt;/h2&gt;

&lt;p&gt;A kill switch can report success while external work continues. Confirm that credentials fail, network paths close, active jobs end where possible and queued work disappears.&lt;/p&gt;

&lt;p&gt;Inspect the running state next. GitOps may restore an approved configuration, while data operations and external API calls can need a separate recovery. Show what completed, stopped halfway and still needs attention.&lt;/p&gt;

&lt;p&gt;Keep these tests in the evaluation set. Include wrong context, unexpected internet access, new destinations, repeated denied actions, lost monitoring and a failed stop attempt.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The manual stop remains available outside the agent runtime.&lt;/li&gt;
&lt;li&gt;Automatic policy checks can stop the task without model cooperation.&lt;/li&gt;
&lt;li&gt;Credentials, network paths, active jobs, queues and retries are checked separately.&lt;/li&gt;
&lt;li&gt;The full activity record remains available after access is removed.&lt;/li&gt;
&lt;li&gt;The running system is compared with the last approved state.&lt;/li&gt;
&lt;li&gt;An engineer confirms recovery before the agent can start again.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;Pressing stop begins the recovery. Verified state finishes it.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Design the stop before the agent starts.&lt;/h2&gt;

&lt;p&gt;Anthropic's report shows how reasonable assumptions can line up badly. The agent believed the prompt, the environment exposed more than intended and monitoring found the activity after real systems had been reached.&lt;/p&gt;

&lt;p&gt;Give every task a named owner, accurate context, limited access, live monitoring, checkpoints and a stop control the agent cannot reach.&lt;/p&gt;

&lt;p&gt;Start with one read-only workflow. Make its work visible, rehearse the kill switch and verify recovery. Add authority from evidence gathered during real runs.&lt;/p&gt;

&lt;p&gt;An engineer should always know what the agent is trying to do, what it can touch and how to stop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://atomsized.com/blog/why-it-is-so-hard-to-trust-ai-with-your-infrastructure" rel="noopener noreferrer"&gt;&lt;strong&gt;How Can You Trust AI Agents With Your Infrastructure?&lt;/strong&gt;&lt;/a&gt; — The context, operating experience, permission boundary and recovery path an infrastructure agent needs before it prepares a change.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://atomsized.com/blog/how-should-ai-monitor-your-infrastructure" rel="noopener noreferrer"&gt;&lt;strong&gt;How Should AI Monitor Your Infrastructure?&lt;/strong&gt;&lt;/a&gt; — How an AI monitoring agent can gather evidence, report useful findings and keep the responsible engineer in control.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Give the agent a visible task and an external stop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can map one AI-assisted workflow, limit its access, show engineers what it is doing and build the stop and recovery path before it reaches production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atomsized.com/contact" rel="noopener noreferrer"&gt;Schedule a platform call&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>How Should AI Monitor Your Infrastructure?</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Thu, 30 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/atomsized/how-should-ai-monitor-your-infrastructure-166d</link>
      <guid>https://dev.to/atomsized/how-should-ai-monitor-your-infrastructure-166d</guid>
      <description>&lt;p&gt;&lt;strong&gt;An alert tells you that something changed. It rarely says why, who is affected or what to do next. The on-call engineer still opens several tools and pieces the story together. AI can help when it understands the platform around the alert.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent collects the evidence and reports the finding. Engineers decide what to do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine an API latency alert. It identifies the graph that moved. It does not explain whether users are blocked, what changed ten minutes earlier or which dependency is slow.&lt;/p&gt;

&lt;p&gt;We have worked through incidents where a deployment looked healthy while a dependency was failing. We have also seen the largest movement on a dashboard lead away from the real user problem. The answer came from connecting the signals to the platform around them.&lt;/p&gt;

&lt;p&gt;An agent that opens dashboards faster saves a few clicks. A useful one follows the incident like an experienced engineer. It knows how the service should behave, how the parts connect, what changed and who owns the response.&lt;/p&gt;

&lt;p&gt;Start with one incident your team already understands. Write down how the investigation works, connect the information it needs and let the agent follow that path in read-only mode.&lt;/p&gt;

&lt;h2&gt;The agent should join the investigation early.&lt;/h2&gt;

&lt;p&gt;Monitoring rules tell the team when a known signal crosses a limit. Someone still works out whether users are affected, which services are involved, what changed and what can be done safely.&lt;/p&gt;

&lt;p&gt;The agent can begin when the alert arrives. It can update the incident notes, collect evidence and prepare a clear summary for the responsible engineer.&lt;/p&gt;

&lt;p&gt;The first guess will often be incomplete. One service may recover while another starts to fail. A recent deployment may look related and turn out to be harmless. The agent should keep checking its explanation as the situation changes.&lt;/p&gt;

&lt;p&gt;What we want is simple: a shorter path from the first signal to a useful understanding of the problem. The engineer should see who is affected, what changed, which dependencies matter, what evidence is missing and what to check next.&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;The agent keeps the incident picture updated. The engineer decides what to do.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Give the agent a clear map of the platform.&lt;/h2&gt;

&lt;p&gt;A large collection of metrics and logs is only the starting point. The agent needs to know how those signals relate to the system the team runs.&lt;/p&gt;

&lt;p&gt;Start with what the user is trying to do. Follow that path through the applications, databases, queues, networks, cloud resources and external services involved. Connect each part to its owner, expected state, deployment source and recovery steps.&lt;/p&gt;

&lt;p&gt;Then add what changed. Git commits, CI runs, Argo CD syncs, Terraform changes, Kubernetes events, AWS activity, feature releases, maintenance and traffic shifts can all change how an alert should be read.&lt;/p&gt;

&lt;p&gt;Add the team's experience as well. Runbooks show the usual response. Incident reviews show what happened here before. Maintenance notes explain temporary conditions. Confirmed results show which clues helped and which guesses were wrong.&lt;/p&gt;

&lt;h3&gt;Connect the user problem to the recovery&lt;/h3&gt;

&lt;p&gt;Each part answers a different question during the incident.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What healthy looks like:&lt;/strong&gt; Describe what users and business processes should be able to complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How the system connects:&lt;/strong&gt; Map the applications, data, networks, cloud resources and external services involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is happening:&lt;/strong&gt; Collect metrics, logs, traces, events and checks with clear names and timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed:&lt;/strong&gt; Connect deployments, configuration, infrastructure changes and planned work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the team knows:&lt;/strong&gt; Add owners, runbooks, maintenance, known exceptions and earlier incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent investigation:&lt;/strong&gt; Build the timeline, test possible causes, show gaps and prepare the next check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineer decision:&lt;/strong&gt; The responsible engineer reviews the evidence and chooses the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the recovery:&lt;/strong&gt; Watch the result, record what happened and improve the next investigation.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;&lt;p&gt;The map should show how a user problem connects to the systems underneath it.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;The agent can only use the context you prepare.&lt;/h2&gt;

&lt;p&gt;Most of the information probably exists already. The problem is that each tool describes the platform differently. A repository may use one service name, Kubernetes another and the cloud account a third. Timestamps may use different zones. The team may know the owner even though the tools do not.&lt;/p&gt;

&lt;p&gt;Connect those sources while keeping a link to the original data. The agent should be able to move from a user-facing check to the service, current deployment, related infrastructure, recent change and owner without guessing what names mean.&lt;/p&gt;

&lt;p&gt;Access also needs care. The agent may read from several systems, but each source still needs its own permissions and audit record. Secrets and unrelated customer data should stay outside the investigation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What healthy looks like:&lt;/strong&gt; Record the user result, expected service behaviour, safe limits and checks that confirm recovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One name for each thing:&lt;/strong&gt; Match services, environments, clusters, accounts, resources and deployments across the tools that use them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service connections:&lt;/strong&gt; Keep the upstream and downstream links across applications, data, network, cloud and external services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring data:&lt;/strong&gt; Collect user checks, metrics, logs, traces and events with clear timestamps and source labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History of changes:&lt;/strong&gt; Put code, configuration, infrastructure, feature, maintenance and traffic changes on one timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team knowledge:&lt;/strong&gt; Connect owners, escalation paths, runbooks, known exceptions and incident results confirmed by engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe access:&lt;/strong&gt; Use a separate machine identity, permissions for each source and a record of what the agent reads or proposes.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;A stronger model cannot fix missing names, broken timestamps or an old service map.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Let the agent write the first investigation draft.&lt;/h2&gt;

&lt;p&gt;This is the approach behind the Atomsized Observability Agent. It reads the approved environment context, then checks Kubernetes workloads, pod status, events, logs, Prometheus alerts, selected AWS resources and spend signals on a schedule.&lt;/p&gt;

&lt;p&gt;Most checks should stay quiet. Normal deployment churn, short pod restarts and spot node rotation only need a report when the behaviour persists or affects a real workload.&lt;/p&gt;

&lt;p&gt;When it finds something worth investigating, the agent writes a short Markdown report in the observability repository and posts it to Slack for review. The report states what happened, why it may matter, which evidence supports it and where the engineer should check next.&lt;/p&gt;

&lt;p&gt;That level of automation removes repeated collection work and keeps the team informed. The responsible engineer reviews the finding, decides whether action is needed and owns the response through recovery.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name the environment, service, affected user result and current owner.&lt;/li&gt;
&lt;li&gt;Describe the finding and show what normal looked like before it.&lt;/li&gt;
&lt;li&gt;Attach the useful evidence with clear sources and timestamps.&lt;/li&gt;
&lt;li&gt;Connect recent deployments, configuration and infrastructure changes.&lt;/li&gt;
&lt;li&gt;Show possible causes, weak links and missing information.&lt;/li&gt;
&lt;li&gt;Suggest the next useful check for the responsible engineer.&lt;/li&gt;
&lt;li&gt;Update the report when the evidence or affected service path changes.&lt;/li&gt;
&lt;li&gt;Record whether the response restored the expected result.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;Automate evidence collection and reporting. Keep production decisions with the responsible engineer.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Keep the response inside the usual delivery and recovery process.&lt;/h2&gt;

&lt;p&gt;The agent may need to read from monitoring, delivery and cloud systems. Changing those systems needs a much smaller set of permissions.&lt;/p&gt;

&lt;p&gt;For Atomsized workflows, AI reads and prepares. Engineers decide and approve. Git records the accepted change. GitOps applies and reconciles it. AWS reports what happened in the running environment.&lt;/p&gt;

&lt;p&gt;The incident summary should name the service owner, incident owner, proposed action, expected result, stop condition and recovery check. If the evidence changes before approval, the proposal should be updated.&lt;/p&gt;

&lt;p&gt;Later, the team may allow one small and reversible response to run automatically. The identity, action limits, retries, time limit and stop conditions should be enforced outside the model. Changes to data, identity and networking should keep a stronger review.&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;The agent prepares the evidence and response. The responsible engineer owns the change and recovery.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Start with one incident your team already understands.&lt;/h2&gt;

&lt;p&gt;Trying to monitor the whole platform with AI creates too much scope for the first version. Choose one service path and one type of incident that experienced engineers already know how to investigate.&lt;/p&gt;

&lt;p&gt;A good starting case has clear user impact, several useful data sources, a known owner and enough past incidents to test against. It should be safe to run with read-only access.&lt;/p&gt;

&lt;p&gt;Write down the investigation before connecting the agent. Follow an engineer from the first alert to the confirmed recovery. Record the tools they open, the questions they ask and the points where missing information slows them down.&lt;/p&gt;

&lt;p&gt;That path becomes the first monitoring skill. In read-only mode, the agent can run the checks on a schedule, prepare the report and show when the evidence points somewhere else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1. Choose the incident:&lt;/strong&gt; Choose the service, type of incident, affected user result, owner and evidence that confirms recovery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2. Map the steps:&lt;/strong&gt; List the tools, data sources, questions and decisions used during the investigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3. Connect the data:&lt;/strong&gt; Give services stable names, align timestamps and provide read-only access to the evidence the agent needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4. Replay past incidents:&lt;/strong&gt; Test the workflow with real incidents and near misses, including cases where the right answer is to wait or ask another owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5. Run reports beside the engineer:&lt;/strong&gt; Let the agent publish live findings for the on-call engineer without changing production or paging more people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6. Expand from results:&lt;/strong&gt; Fix missing context and add another type of incident only after the first investigation path is dependable.&lt;/li&gt;
&lt;/ul&gt;

&lt;ul&gt;
&lt;li&gt;One person who owns the monitoring workflow and one owner for the service.&lt;/li&gt;
&lt;li&gt;A written description of what healthy looks like for users, the service and its dependencies.&lt;/li&gt;
&lt;li&gt;A service map connected to current ownership and escalation.&lt;/li&gt;
&lt;li&gt;One timeline for monitoring data, application changes and infrastructure changes.&lt;/li&gt;
&lt;li&gt;A read-only agent identity with limited access and an audit trail.&lt;/li&gt;
&lt;li&gt;A set of real incidents with results confirmed by engineers.&lt;/li&gt;
&lt;li&gt;One incident-summary format that shows evidence, missing information and other possible causes.&lt;/li&gt;
&lt;li&gt;A review after the shadow period with a clear choice to improve, expand or stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;One complete incident path teaches the team more than a broad monitoring demo.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Check whether the agent makes the investigation easier.&lt;/h2&gt;

&lt;p&gt;A clear explanation can still send the engineer in the wrong direction. Judge the agent by the help it provides during the investigation and by the evidence attached to its conclusions.&lt;/p&gt;

&lt;p&gt;During replays and live shadow mode, compare its incident picture with the one built by the engineer. Record where it found useful information sooner, where it followed a weak link and where missing ownership or an old service map blocked it.&lt;/p&gt;

&lt;p&gt;After each incident, the engineer confirms the cause, useful evidence, chosen response and recovery. Add those confirmed facts to the runbook, incident history and tests used by the monitoring skill.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time from the first alert to a useful incident summary.&lt;/li&gt;
&lt;li&gt;Conclusions that include a source, timestamp and affected service path.&lt;/li&gt;
&lt;li&gt;Missing information found before the agent recommends a response.&lt;/li&gt;
&lt;li&gt;Weak links or confident guesses the engineer needs to correct.&lt;/li&gt;
&lt;li&gt;Checks and responses accepted, changed or rejected by the owner.&lt;/li&gt;
&lt;li&gt;Recovery confirmed and recorded after the response.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;The agent should learn from what the team confirmed after the incident.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Build useful context first.&lt;/h2&gt;

&lt;p&gt;AI monitoring becomes useful when the agent can see the platform around the alert. It needs to know what healthy looks like, how services connect, what changed, who owns the response and what happened in similar incidents.&lt;/p&gt;

&lt;p&gt;It should keep one clear incident picture from the first signal to recovery. Engineers can see what is happening, which possible causes fit the evidence, what remains unknown and what to check next.&lt;/p&gt;

&lt;p&gt;Start with one repeated investigation and run the agent beside the on-call engineer. Let it automate evidence gathering and reports while the team fixes the missing context that real incidents expose. Add another incident type when the first one is dependable.&lt;/p&gt;

&lt;p&gt;Engineers remain responsible for decisions and production changes. The agent helps them reach those decisions with better information and less searching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://atomsized.com/blog/why-it-is-so-hard-to-trust-ai-with-your-infrastructure" rel="noopener noreferrer"&gt;&lt;strong&gt;How Can You Trust AI Agents With Your Infrastructure?&lt;/strong&gt;&lt;/a&gt; — The context, operating experience, permission boundary and recovery path an infrastructure agent needs before it prepares a change.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://atomsized.com/blog/why-gitops-is-hard-to-explain-and-automate" rel="noopener noreferrer"&gt;&lt;strong&gt;Why GitOps is so hard to explain and automate.&lt;/strong&gt;&lt;/a&gt; — A practical operating model for proposals, engineer approval, reconciliation and evidence across AWS and Kubernetes.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Start the Observability Agent with one useful report.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can map what healthy looks like, connect the evidence and automate a report your engineers can review before deciding what to do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atomsized.com/contact" rel="noopener noreferrer"&gt;Schedule a platform call&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>observability</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>From Shared Staging to Reviewable Environments</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/atomsized/from-shared-staging-to-reviewable-environments-gnc</link>
      <guid>https://dev.to/atomsized/from-shared-staging-to-reviewable-environments-gnc</guid>
      <description>&lt;p&gt;&lt;strong&gt;One team deploys a feature for QA. Another needs to test a migration. Product wants a stable demo. Each request makes sense. A single shared environment forces them into the same queue.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shared staging becomes a queue when several teams need it at the same time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have seen one deployment overwrite another, testers wait for a stable slot, demos block development work and teams lose track of which revision is running.&lt;/p&gt;

&lt;p&gt;The name varies between teams: staging, stage, dev, test or UAT. We use shared staging to mean any environment where several changes have to take turns.&lt;/p&gt;

&lt;p&gt;When several decisions wait for the same environment, they have to happen one after another. Engineering, QA and product repeat tests, screenshots and approvals, coordinate deployment windows and wait longer to move a change forward.&lt;/p&gt;

&lt;p&gt;Extra permanent environments rarely clear that queue. A reviewable environment has one job: help someone make a specific decision about a known version of a change.&lt;/p&gt;

&lt;h2&gt;Shared staging slows down when reviews run in parallel.&lt;/h2&gt;

&lt;p&gt;Waiting and unclear targets cause the delay. A team loses confidence in a review when the target changes underneath it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Changes collide:&lt;/strong&gt; A newer deployment replaces the version somebody else is still testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review waits:&lt;/strong&gt; QA, product and engineering negotiate for a stable window instead of reviewing the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results become unclear:&lt;/strong&gt; A test result or screenshot may no longer match what is currently deployed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Measure the wait as part of delivery time.&lt;/h2&gt;

&lt;p&gt;A staging queue often starts before the deployment metrics begin. We look at how long a change waits for its first useful review, how much work is repeated and how often a newer deployment makes earlier results out of date.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Review delay:&lt;/strong&gt; Track the median time from ready for review to the first useful validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time spent waiting:&lt;/strong&gt; Record engineering, QA and product hours spent waiting, coordinating or repeating work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeated review work:&lt;/strong&gt; Count tests, demos and approvals that need to be repeated after a newer deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;Measure one application or service for two weeks. Compare its results over time instead of combining data from teams that work in different ways.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Build the smallest environment the review needs.&lt;/h2&gt;

&lt;p&gt;A reviewable environment has a clear purpose, a known source revision, an owner, clear rules for test data, visible health and a cleanup rule. It only needs the parts required for that decision.&lt;/p&gt;

&lt;p&gt;A preview environment gives developers, testers, product teams and stakeholders their own reviewable version of a change. They can develop, test and demonstrate it without competing for shared staging.&lt;/p&gt;

&lt;p&gt;When previews reuse existing Kubernetes capacity and shared platform services, the extra running cost can stay low. A short-lived namespace uses the cluster and services that are already running.&lt;/p&gt;

&lt;p&gt;That changes when a preview needs dedicated nodes, databases, load balancers, large data sets or a long lifetime. Clear resource limits and automatic cleanup after a set time help keep costs under control.&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;Keep staging production-like. Preview environments should use the same built application versions, configuration patterns and deployment path, while scaling down safely and keeping production data out.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Preview, UAT and shared staging answer different questions.&lt;/h2&gt;

&lt;p&gt;Each stage supports a different kind of review.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preview environment:&lt;/strong&gt; Review one meaningful change quickly and in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinated UAT:&lt;/strong&gt; Test a fixed combination of services, repositories or business flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared staging:&lt;/strong&gt; Keep it for final release checks and tests that need the full system in a common state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Record each step from creation to deletion.&lt;/h2&gt;

&lt;p&gt;A pull request records the change. CI builds a fixed application version. Git records how the environment should be set up. Argo CD applies that setup and keeps it in sync. The URL, version, health and test results return to the pull request.&lt;/p&gt;

&lt;p&gt;When the change is merged, closed or reaches its time limit, the environment definition is removed and the environment is cleaned up. Git history and review results remain available.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with the workflow that waits for shared staging most often.&lt;/li&gt;
&lt;li&gt;Build one complete create, update, review and delete lifecycle.&lt;/li&gt;
&lt;li&gt;Measure waiting time, failed environments and old environments that were not removed before adding more teams or services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Fund one review path, then decide from the results.&lt;/h2&gt;

&lt;p&gt;Choose one staging bottleneck where work regularly waits or needs to be repeated. Run a 30-day pilot, then decide whether the team should spend more time and money on it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline:&lt;/strong&gt; Measure the current wait, repeated work and environment cost for one review path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pilot:&lt;/strong&gt; Build one complete create, update, review and delete lifecycle using the existing delivery controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide:&lt;/strong&gt; After 30 days, extend, adjust or stop based on review delay, repeated work, stability and AWS spend.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;&lt;p&gt;After 30 days, ask whether reviews moved sooner and whether the workflow kept stability and AWS spend under control.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Start with a review that regularly waits.&lt;/h2&gt;

&lt;p&gt;Find the change that repeatedly waits for staging, give it one controlled review path and measure whether the queue gets shorter.&lt;/p&gt;

&lt;p&gt;Shared staging keeps a clear job: final release validation and shared checks. Preview and UAT paths handle review work that needs isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://atomsized.com/blog/preview-environments-monorepo-five-person-team" rel="noopener noreferrer"&gt;&lt;strong&gt;Preview environments for a monorepo team of five or more.&lt;/strong&gt;&lt;/a&gt; - A practical GitOps design for isolated, disposable application states when one repository contains the complete source state.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://atomsized.com/blog/preview-environments-microservices-ten-repositories" rel="noopener noreferrer"&gt;&lt;strong&gt;Preview environments for a microservice architecture with 10+ repositories.&lt;/strong&gt;&lt;/a&gt; - A GitOps operating model for single-service and coordinated previews across multiple repositories.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Turn one blocked review path into a controlled workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can map the smallest useful preview or UAT workflow around your repositories, GitOps controls and current AWS platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://atomsized.com/contact" rel="noopener noreferrer"&gt;Schedule a platform call&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gitops</category>
      <category>devops</category>
      <category>platformengineering</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>What is HTTP, HTTPS, and SSL</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Mon, 08 Nov 2021 13:19:34 +0000</pubDate>
      <link>https://dev.to/boiliev/what-is-http-https-and-ssl-1924</link>
      <guid>https://dev.to/boiliev/what-is-http-https-and-ssl-1924</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;If you are a developer, you probably know what an &lt;code&gt;SSL&lt;/code&gt; certificate is. If you don't know what it is, don't worry. An &lt;code&gt;SSL&lt;/code&gt; certificate is a must-have on any website. It lets your web browser connect to a web server securely. &lt;code&gt;SSL&lt;/code&gt; stands for &lt;strong&gt;Secure Sockets Layer&lt;/strong&gt;, and it is a digital certificate that allows for an encrypted connection.&lt;/p&gt;

&lt;p&gt;But let's back it up a bit. First, we need to know what lets a web browser and a webserver connect. To connect with a website, we need &lt;code&gt;HTTP&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is &lt;code&gt;HTTP&lt;/code&gt;?
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;HTTP&lt;/code&gt; stands for &lt;strong&gt;Hypertext Transfer Protocol&lt;/strong&gt; and this protocol is used to transfer data from the web servers to web browsers. So if you ever visit a website you can notice the protocol being added at the beginning of the web address. So for example &lt;code&gt;http://www.website.com&lt;/code&gt;. This protocol is used to view different web pages on the internet.&lt;/p&gt;

&lt;p&gt;By default in standard &lt;code&gt;HTTP&lt;/code&gt;, all of the information is sent in clear text. What this means is when you type something, it gets sent over the public internet which means that hackers can view it, because it's all sent over in clear text.&lt;/p&gt;

&lt;p&gt;This isn't a problem, that is if you only browse a website, but if there are any passwords involved, or any other personal information, such as credit card information, then a hacker could easily get that info. Due to the fact it's being sent over the public internet, a hacker could listen in as the data is being transferred and could steal it.&lt;/p&gt;

&lt;p&gt;If you ever have to type in personal information, you should always check if you have a secure connection. And for you to have a secure connection, you would need to use &lt;code&gt;HTTPS&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is &lt;code&gt;HTTPS&lt;/code&gt;?
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;HTTPS&lt;/code&gt; is &lt;code&gt;HTTP&lt;/code&gt;, but secure. That's why there is an &lt;code&gt;S&lt;/code&gt; added. &lt;strong&gt;Hypertext Transfer Protocol Secure&lt;/strong&gt;. What this does is encrypt the data that is being retrieved by &lt;code&gt;HTTP&lt;/code&gt;. It lets all of the data that is being transferred between the server and the computer secure. The way it does that is by making the data impossible to read by encryption algorithms that mix up the data that's being transferred.&lt;/p&gt;

&lt;p&gt;So whenever you are on a site and you have to enter a password or some credit card information, make sure the web address looks like this - &lt;code&gt;https://www.website.com&lt;/code&gt;. This means that the data that you pass in will be protected. You can also notice a padlock to the left of the web address, which also indicates you are using a secure &lt;code&gt;HTTP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to the encryption algorithms the data we put in comes out all scrambled up so that hackers can't understand and decrypt it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HTTPS&lt;/code&gt; secures the data thanks to a protocol called:&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;code&gt;SSL&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;SSL&lt;/code&gt; stands for &lt;strong&gt;Secure Sockets Layer&lt;/strong&gt; and it is a protocol that's used to establish security on the internet. But how exactly does &lt;code&gt;SSL&lt;/code&gt; work? Well, it uses public-key encryption to secure data.&lt;/p&gt;

&lt;p&gt;What this means is when a computer connects with a website that is using &lt;code&gt;SSL&lt;/code&gt;, the computer's web browser will ask the website to say who it is or identify itself. Then the webserver will send the computer a copy of the &lt;code&gt;SSL&lt;/code&gt; certificate it has.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;SSL certificate&lt;/code&gt; is a small digital certificate that is used to validate the identity of a website. Or in other words, this lets your computer know that the website you're visiting is reliable and trustworthy.&lt;/p&gt;

&lt;p&gt;So then the browser of the computer can check if it trusts the certificate and if it does it will send a message to the web server, to which then the web server will respond with a response so that an &lt;code&gt;SSL&lt;/code&gt; session can begin.&lt;/p&gt;

&lt;p&gt;Most &lt;code&gt;SSL&lt;/code&gt; certificates have to be paid for, but there are also some free ones, for example:&lt;/p&gt;

&lt;h1&gt;
  
  
  Let's Encrypt
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;Let’s Encrypt&lt;/code&gt; is a Certificate Authority (CA) that provides an easy way to obtain and install free &lt;code&gt;SSL&lt;/code&gt; certificates, thereby enabling encrypted HTTPS on web servers. You could check this post out on &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04" rel="noopener noreferrer"&gt;How To Secure Nginx with Let's Encrypt on Ubuntu&lt;/a&gt; so that you can get a little bit more familiar with &lt;code&gt;Let's Encrypt&lt;/code&gt;. &lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This may seem confusing at first, but it's best to get to know how the internet actually works and the different protocols that are out there. If you notice almost all of the sites nowadays have &lt;code&gt;HTTPS&lt;/code&gt; instead of &lt;code&gt;HTTP&lt;/code&gt;, even though there isn't any data to be transferred between servers, and that's because Google has started flagging websites as &lt;strong&gt;Not Secure&lt;/strong&gt; because they aren't protected with &lt;code&gt;SSL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I hope that this post has helped you learn a little bit more about these protocols that you use daily and how they work.&lt;/p&gt;

</description>
      <category>http</category>
      <category>https</category>
      <category>ssl</category>
    </item>
    <item>
      <title>How to get the size of a directory in Linux</title>
      <dc:creator>Boyan Iliev</dc:creator>
      <pubDate>Thu, 04 Nov 2021 09:17:37 +0000</pubDate>
      <link>https://dev.to/boiliev/how-to-get-the-size-of-a-directory-in-linux-3bhd</link>
      <guid>https://dev.to/boiliev/how-to-get-the-size-of-a-directory-in-linux-3bhd</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Linux is one of the most popular operating systems in the world. It is Unix-like, and it is also open-source. Quite a big percentage of developers use Linux because it can be customized in so many ways.&lt;/p&gt;

&lt;p&gt;What's cool about Linux is its command line. Every 'hackers' paradise. There are a ton of commands. If you aren't familiar with any Linux commands, be sure to check out this post on the &lt;a href="https://devdojo.com/bo-iliev/8-linux-commands-everyone-should-know" rel="noopener noreferrer"&gt;8 Linux Commands Everyone Should Know&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is probably a command for almost anything that you want to do. For example what if you wanted to get the size of a directory? Well luckily for you there is a command for that. In this post, we are going to talk about that command and see what it can offer.&lt;/p&gt;

&lt;h1&gt;
  
  
  The &lt;code&gt;du&lt;/code&gt; command
&lt;/h1&gt;

&lt;p&gt;This command lets the user get a quick view of the &lt;strong&gt;disk usage&lt;/strong&gt;. The best way to use it is by giving it the directory you want to see the size of. It should look a little something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;du directory_name

// output
2314    directory_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will give you the size of all the files and at the very end, it will give you the size of the directory itself. You could point out the full path, or you could just give the name of the directory you want to see if you are already on the same path.&lt;/p&gt;

&lt;p&gt;But we could make this even easier just b adding 2 &lt;strong&gt;flags&lt;/strong&gt;. Flags help the command change its behavior. For the &lt;code&gt;du&lt;/code&gt; command we could add the flags &lt;code&gt;-s&lt;/code&gt; and &lt;code&gt;-h&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-s&lt;/code&gt; stands for &lt;strong&gt;summarize&lt;/strong&gt; and it will show you only the total size of the directory, without all those files popping up on your screen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-h&lt;/code&gt; stands for &lt;strong&gt;human-readable&lt;/strong&gt; and it will convert the size so that you can read it easier. Just by running the command without the &lt;code&gt;-h&lt;/code&gt; flag doesn't specify you a unit of measurement.&lt;/p&gt;

&lt;p&gt;So now to get the best of this command, you should run it like this with the &lt;code&gt;-s&lt;/code&gt; and &lt;code&gt;-h&lt;/code&gt; flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;du -sh directory_name

// output
44.5M   directory_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you wanted to see all of the directories sizes, you could just run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;du -sh ./*

// output
12.2M   dir1
 2.5M   dir2
  55M   dir3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another thing you can do is use pipe(&lt;code&gt;|&lt;/code&gt;) and sort them by size, which will make it even easier for you to see their sizes. What &lt;code&gt;|&lt;/code&gt; does is get the output from the command behind it and add it into the input of the command after it. So to sort the directories by size just run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;du -sh ./* | sort -h

// output
  55M   dir3
12.2M   dir1
 2.5M   dir2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This is a pretty useful command that I think everybody should know, just because of how short and easy it is. I hope that this post has helped you and I wish you happy coding.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>terminal</category>
      <category>codenewbie</category>
    </item>
  </channel>
</rss>
