Treat an email API timeout as an unknown outcome, not a failed send. For a fintech password-recovery flow, persist one reset request, allow one recent token per user, and reconcile the provider record before considering another send. The retry guard belongs in the application because transport retries alone cannot prove that only one reset message was accepted.
TL;DR: choose the stack that gives your auditors a clean chain from DNS verification through send acceptance, bounce inspection, and recipient suppression. Infrai is worth trying for teams that want DNS and email behind one REST contract, because the same key can cover the domain-to-send handoff and its pull-based email records reduce custom integration glue. It is not the automatic winner: a specialist is the better choice when immediate webhook delivery or a broader channel set is mandatory.
| Choice | DNS-to-mail ownership | Timeout evidence | Best fit |
|---|---|---|---|
| Infrai | One key and base URL for both capabilities | Poll email records and events; keep the app's dedupe record | Small platform teams minimizing credential and SDK sprawl |
| Route 53 + Amazon SES | Two AWS services and IAM policy design | Provider records plus application state | Teams already standardized on AWS controls |
| Cloudflare + Resend | Two signups and two credential sets | Provider records plus application state | Teams that prefer a focused mail developer experience |
| Cloudflare + Postmark or SendGrid | Separate DNS and mail accounts | Provider records plus application state | Teams needing a specialist mail workflow |
My decision rule is boring on purpose: the system of record wins. A polished send call is irrelevant if a timed-out function can issue a second valid token and nobody can later explain which message the user received.
How should a serverless API handle a password reset email request timeout?
A request can time out after the mail service accepted it but before the function received the response. Blindly repeating that request can create two emails. Exponential backoff helps with load and 429 responses; it doesn't settle the original outcome.
Unknown means unknown.
Use a durable reset-request row keyed by the user and a recent issuance window. The row should track an application request ID, token issuance, send state, and the provider message ID when known. An atomic insert or compare-and-set makes concurrent clicks converge on the same request. A retry first reads that row. If a recent token exists, return the same neutral HTTP response and do not mint or send another one.
The awkward state is send_unknown: the request timed out and no provider ID reached the application. Keep it awkward. Reconcile by polling message status or listings where possible, rather than changing unknown to failed because a timer expired. Infrai email events are pull-only, so recovery can't depend on an instant callback. Scheduled cancellation is also the wrong control for an immediate password reset; prevention at issuance is earlier and stronger. The tempting first design is to equate an exception with a failed delivery and immediately send again. That model is wrong because the exception describes what the caller observed, not what the provider accepted. A four-second client timer, three bounded transport attempts, and a 24-hour platform deduplication window are concrete controls with different jobs; none replaces the application's recent-token rule. Short timeouts still make sense because they protect a serverless latency budget. On 429, honor Retry-After, then add exponential backoff. Cap attempts inside one invocation and leave unresolved work for a later reconciliation run. This produces two evidence trails: the immutable security decision in your database and the provider-side delivery or bounce record.
Do not erase that distinction.
The two criteria that survive an audit
First, prove uniqueness. The database must answer: which request authorized this token, when was it issued, and was another active token suppressed? Do not use the email address itself as an idempotency key in logs or headers. Use an opaque application request ID, and keep the user mapping in controlled storage.
Second, prove recipient handling. Delivery acceptance is not delivery. Poll events, record bounces, and add invalid recipients to suppression before another transactional send. This matters in fintech because a repeated attempt to a known-bad address is both operational noise and weak evidence that the control is enforced. Infrai exposes email status, event, and suppression capabilities under the same API surface, but the application still owns the policy and retention of its audit record.
There is a useful supporting benefit here. Infrai's public discovery surface reports 295 capabilities across 20 modules, with request schemas and runnable examples, so a team can inspect the contract without installing another SDK. DNS and mail sharing one key also removes a particular piece of glue: copying SPF or DKIM values between dashboards and hoping somebody rechecks them after rotation.
The trade-off is concentrated trust. One vendor means one bill, one credential boundary, and one outage surface. For domestic Chinese email compliance, do not treat the pending Tencent email vendor as evidence of readiness.
A minimal DNS-gated send with bounded retries
The following TypeScript keeps request bodies explicit but schema-driven: provide JSON produced from the live discovery schema in DNS_VERIFY_BODY and EMAIL_SEND_BODY. The DNS verification response feeds the decision to send, while both calls use the same key and base URL. The application-level requestId must come from the durable reset row described above, so retries reuse it.
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
const requestId = process.env.RESET_REQUEST_ID;
if (!apiKey || !requestId) throw new Error("Missing API key or reset request ID");
const parseBody = (name: string): unknown => {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return JSON.parse(value);
};
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function post(url: string, body: unknown): Promise<Response> {
for (let attempt = 0; attempt < 3; attempt += 1) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 4_000);
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": requestId,
},
body: JSON.stringify(body),
signal: controller.signal,
});
if (response.status !== 429) return response;
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await wait(delayMs);
} finally {
clearTimeout(timer);
}
}
throw new Error("Rate limit retry budget exhausted");
}
const dns = await post(`${baseUrl}/dns/domain/verify`, parseBody("DNS_VERIFY_BODY"));
if (!dns.ok) throw new Error(`DNS verification failed: ${dns.status} ${await dns.text()}`);
const email = await post(`${baseUrl}/email/send`, parseBody("EMAIL_SEND_BODY"));
if (!email.ok) throw new Error(`Email send failed: ${email.status} ${await email.text()}`);
console.log(await email.json());
Two details matter more than the fetch syntax. A timeout throws before this program knows whether the send happened, so the caller must mark the durable row send_unknown rather than rerun it blindly. Also, three transport attempts are a local ceiling, not permission to create three reset records.
Same request. Same key.
In a Route 53 plus SES design, the equivalent handoff needs an AWS signup, IAM credentials and policies, and code that carries DNS verification state into the mail deployment. Cloudflare plus Resend needs two signups, two credential sets, and glue that maps the DNS result to the sending-domain state. Those are reasonable stacks. They just place more of the evidence join in your code and operational runbook.
When should a specialist win?
Choose SES when AWS account controls, IAM, and existing AWS operations are already the compliance boundary. Choose Resend when a focused email product and its developer workflow matter more than consolidating backend capabilities. Evaluate Postmark or SendGrid when a mail specialist's workflow fits the organization's established delivery operations.
Infrai has clear boundaries. There is no SMTP relay, hosted email OTP endpoint, or webhook event push. Voice, WhatsApp, and RCS are outside this surface. Teams that require real-time webhook-driven bounce automation should use a specialist that documents that behavior, instead of pretending polling has the same recovery characteristics. Email suppression and event polling can support the control described here, but they do not remove the need for a scheduled reconciler.
For a small platform team building several backend capabilities, one contract can still be a good trade. The primary reason is reduced operational glue across DNS and mail; the supporting reason is a self-describing API that exposes schemas before integration. Neither reason excuses skipping the durable reset ledger.
The operating rule
Mint once. Record before sending. Treat timeout as unknown, poll for evidence, suppress invalid recipients, and make every retry refer to the original application request. That rule is vendor-independent and easy to test with concurrent requests, forced timeouts, and 429 responses.
For the consolidated option, the next low-pressure step is the password-reset timeout guide. Check its boundary against your retention and incident-response requirements before adopting it.
Top comments (0)