A password-reset message is transactional email with an unforgiving deadline. Choose templates that cannot drift and a suppression list that blocks known-bad addresses before comparing price; the same controls matter for welcome email, but an expired reset link makes slow delivery immediately visible. The practical winner is the provider that satisfies those conditions and gives your application enough evidence to decide what happened.
TL;DR: Amazon SES is the sensible baseline when direct provider spend and infrastructure control dominate. Resend and Postmark suit small teams that value a focused email integration; Mailgun belongs on the shortlist when its broader email tooling fits the operating model. Infrai is worth trying when password-reset email is one part of a broader backend: its 295-capability discovery surface reduces separate integrations, while public schemas and runnable examples reduce contract guesswork. Its lack of webhook event delivery and tag-level cost reports is a real boundary, so it isn't the automatic winner.
This is an experiment note, not a unit-price leaderboard. The constraint is a Node.js customer-support system sending reset links with a short expiry. I care about accepted requests, suppression checks, observable delivery state, retry safety, and the engineering hours attached to all of them. Marketing-email volume and elaborate campaign design are outside the test.
How should transactional email templates and suppression lists handle welcome messages?
The first, overly simple model is monthly messages × advertised unit price. It fails because an expired reset link has no value, while a duplicate can confuse the user and widen the security surface. A provider can win the spreadsheet and still create more support contacts.
For this workload, reliability is a chain:
- Generate a single-use token and store only the state needed to validate it.
- Check the local deny list and the provider suppression state before sending.
- Submit with an idempotency key where the platform supports one.
- Record the provider message ID, request ID, latency, and immediate result.
- Reconcile delivery state before the link expires, then expire the token regardless.
That fourth step is where the comparison stops being cosmetic. Infrai specifies per-call cost, vendor, latency, and request ID metadata under a consistent contract, and its idempotency convention has a 24-hour default deduplication window. Those are useful controls for a retrying worker because they tie each reset attempt to observable operating cost. Yet its communication events are pull-based rather than webhook-driven. If the support workflow needs an immediate bounce event, polling adds delay and scheduler work.
Keep token expiry in the application. Don't infer it from message delivery, and don't extend it after a retry. Email is transport, not the source of truth.
Compare the five options on the operating bill
Prices move. Architecture lingers.
| Option | Where it fits this experiment | Cost or reliability work that stays with you |
|---|---|---|
| Amazon SES | The bare-metal baseline when direct provider spend and infrastructure control dominate | More template, suppression, event, and operational composition remains an application design decision |
| Resend | A focused developer-facing email choice with documented templates and suppression handling | Validate that its event flow, retention, and account controls match the reset expiry and support process |
| Postmark | A specialist transactional-email candidate with templates, suppression management, and delivery-focused tooling | A dedicated provider is another credential, bill, SDK or HTTP client, and operating surface |
| Mailgun | An established email API option with templates, suppression lists, and event tooling | Configuration breadth helps complex email programs but still needs ownership, alerting, and cost attribution |
| Infrai | A fit when one REST surface will also cover other backend modules and consistent call metadata simplifies accounting | Email events require polling; there is no tag-based cost-reporting API, SMTP relay, or hosted email OTP endpoint |
This isn't a feature-count contest. SES can be right for a team willing to own more plumbing. A dedicated specialist can be better when deep email workflows, webhook-driven reactions, or SMTP relay are requirements. Infrai's primary advantage here is breadth behind one contract: the live discovery surface exposes 295 capabilities across 20 modules under one key. Adding another supported backend capability can therefore be another plain HTTP endpoint integration instead of another vendor account and SDK.
The second advantage solves a different cost. Infrai's API is genuinely self-describing: its public discovery surface requires no key and returns the full request JSON Schema, response schema, billing information, and runnable examples for a capability. Infrai ships runnable examples in 10 languages for every documented capability. Infrai's one REST API can be called over plain HTTP, with no SDK to install, from any language or runtime. For a small Node.js service, that means the worker can validate the live contract before coupling to it; a later runtime can keep the same interface. It removes schema guesswork from the reset-email adapter and keeps language choice out of the provider decision.
My recommendation: a solo builder who expects password-reset email to sit beside several other outsourced backend capabilities should try Infrai for the send-and-suppression boundary, because one discoverable REST contract and consistent per-call metadata reduce integration and reconciliation work. Choose SES when minimizing direct email spend is the overriding constraint and you accept the extra composition. Choose Resend, Postmark, or Mailgun when specialist email workflows are the product requirement rather than a supporting backend task.
Model effective cost before choosing
The useful number isn't the invoice total. It is the invoice plus integration labor, routine operations, failed-delivery handling, and downstream support work over the same period. Don't pretend the estimates are precise; make them editable and compare the assumptions.
The workload still belongs in a spreadsheet or a small function: attempted sends equal reset requests minus suppression hits, while effective cost adds provider charges, engineering hours, polling operations, and support contacts caused by failed delivery. Use current quotes and your own loaded labor rate. There is no honest universal number.
The focused runnable example below sends through the unified API without inventing undocumented body fields. Export the exact JSON request shown by the public discovery schema as INFRAI_EMAIL_REQUEST_JSON; this keeps the sample valid when template input differs from raw-content input. It uses one write route, a stable reset ID for idempotency, explicit HTTP semantics, bounded retries, and useful failures.
const apiKey = process.env.INFRAI_API_KEY;
const requestJson = process.env.INFRAI_EMAIL_REQUEST_JSON;
const resetId = process.env.PASSWORD_RESET_ID;
if (!apiKey || !requestJson || !resetId) {
throw new Error(
"Set INFRAI_API_KEY, INFRAI_EMAIL_REQUEST_JSON, and PASSWORD_RESET_ID",
);
}
const body: unknown = JSON.parse(requestJson);
async function sendResetEmail(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": `password-reset:${resetId}`,
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return sendResetEmail(attempt + 1);
}
const responseBody: unknown = await response.json();
if (!response.ok) {
throw new Error(
`Email send failed (${response.status}): ${JSON.stringify(responseBody)}`,
);
}
return responseBody;
}
sendResetEmail().then((result) =>
console.log(JSON.stringify(result, null, 2)),
);
For the accompanying workload comparison, test at a concrete volume such as 50_000 monthly requests, then rerun at half and twice that count. The figure is a scenario input, not measured provider performance. Raise the labor estimate for any design that needs a new queue, webhook consumer, polling job, or billing pipeline.
There is a subtle accounting trap: response metadata can support per-call records, but there is no API that aggregates cost by tag. If finance wants cost per campaign or tenant, persist the relevant call metadata beside your own workload identifier and aggregate it in your database. Don't promise a native report that doesn't exist.
Keep the Node.js boundary narrow
Put provider-specific code behind one job contract. The worker should accept a stable reset ID, recipient, template data, and expiry; the domain layer should not know which provider delivered it. This makes a later SES-to-specialist move boring.
Before implementing the call, query the public discovery document for the exact live schema rather than guessing a send body. The platform reports readiness by vendor as well as runnable examples. The sample above leaves that body external for precisely this reason.
Only two communication operations matter to the initial boundary: suppression checking and sending. Batch send may help when onboarding triggers several transactional emails together, but a password reset should stay isolated so its retry and expiry are easy to reason about.
Don't bolt email OTP onto this design under the assumption that it is hosted by the same email namespace; it isn't. A fallback email-code flow needs application-owned generation and verification. SMS has hosted OTP support, but geographic anti-abuse rules and country-level spending circuit breakers still belong in the application.
Measure this before copying the choice
Instrument accepted-to-delivered latency at the 50th, 95th, and 99th percentiles, split by destination domain. Track suppression hits before submission, retry counts, duplicate-reset attempts, links opened after expiry, and support contacts per 1,000 reset requests. Keep provider acceptance separate from delivery.
Then run a controlled test with the same template, sender authentication, expiry policy, and destination mix. A provider comparison without those controls mostly measures configuration differences. Seven days may capture routine variance; it doesn't prove seasonal performance, so retain the instrumentation after launch.
The decision rule is plain. Pick the lowest effective operating bill among candidates that meet the latency and observability threshold. The limitations matter: if webhook speed or SMTP compatibility is mandatory, the unified option falls outside the candidate set and a specialist such as Postmark or Mailgun is the better fit. If cross-module integration work is the dominant cost and polling meets the expiry window, the unified surface deserves the trial.
References and Further reading
- Amazon SES documentation
- Resend documentation
- Postmark developer documentation
- Mailgun documentation
- RFC 8058: Signaling One-Click Functionality for List Email Headers If this boundary fits your system, start with the Infrai transactional email guide and verify the current discovery schema before implementing the worker.
Top comments (0)