{
  "id": 4583312,
  "title": "Why your transactional email needs a queue, not a try/catch",
  "url": "https://urgent.news/2026/08/31/why-your-transactional-email-needs-a-queue-not-a-try-catch",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-08-31T06:04:25.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/greg_pabijanmorawski/why-your-transactional-email-needs-a-queue-not-a-trycatch-4di4"
  },
  "original_language": "en",
  "account": "In many existing codebases, sending email is handled similarly: within a POST handler, between writing to the database and sending the response, there's an await on the mail provider's SDK. This approach works for a while, but eventually, issues arise, such as the signup endpoint timing out. It takes an hour to determine that the problem is caused by the email provider experiencing issues far away from the user's location. The author, who built Pulsenote, a transactional email API, has spent a significant amount of time exploring the space between the API call returning 200 and the message reaching the recipient's inbox. This article delves into that gap, explaining why a try/catch block is insufficient and where the line lies between using a simple pipeline and overengineering a side project.\n\nThe naive implementation includes a single line of code that sends the email right after creating the user in the database. This straightforward approach is often sufficient for many applications, as stated at the end of the article. However, it's crucial to understand the costs associated with this method, as it's not merely fine but has measurable implications.\n\nBy incorporating a third-party email provider into the request path, the p99 response time for the POST /signup endpoint increases, becoming a combination of the provider's p99. When the provider slows down, the endpoint's performance deteriorates, potentially leading to timeouts and service failures. If the provider returns a 5xx status code, the email is not sent at all. The catch block in the code snippet can either rethrow the error, causing a 500 response for a successfully created user, or suppress the error, leading to a missing email without any record or retry mechanism.\n\nThere's no built-in retry mechanism, and AWS SES returns a ThrottlingException when the account's sending rate is exceeded. Inside an HTTP handler, there's no waiting mechanism or idempotency, meaning the client's HTTP retry after a timeout could result in duplicate emails or failures in tracking. Logging errors silently creates a hidden issue, while fire-and-forget methods launch multiple concurrent sends, causing throttling errors and making it difficult to track what was sent.\n\nTo address these problems, the recommended solution is to split the operation at the point where a promise is made to the caller. The HTTP handler should only perform the necessary work to accept responsibility for a message, then return. The actual delivery of the message occurs elsewhere, on its own schedule, with its own error handling. This boundary between acceptance and delivery is the core concept. Other components, such as authentication, message insertion, queueing, and processing, are implementation details that follow.",
  "summary": "Almost every codebase I've inherited sends email the same way: somewhere inside a POST handler, between the database write and the response, there's an await on the mail provider's SDK. It works, for months. Then one afternoon your signup endpoint starts timing out, and it takes an hour to work out that the cause is your email provider having a bad day three thousand kilometres away. I build…",
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}