Short answer: issue one reset token, record that decision atomically, and let retries reuse the same send operation. A timeout is an unknown result, not permission to mint another token or fire another email. For a fintech contact flow, keep the reset template owned by the authentication path; the support router may classify the contact and select a queue, but it should never become a second sender.
The least complex safe design has two layers. The application deduplicates by user and recent token issuance. The delivery request carries a stable idempotency key. That split matters because a provider can deduplicate HTTP retries, but it cannot know that two newly generated tokens came from one human click.
There is one more boundary people skip: DNS and mail configuration. A reset flow is not ready merely because a send request returned. The sending domain has to remain verified after DNS changes, and that state should gate deployment or sending rather than live in a setup document nobody revisits.
How should a password reset email API handle a request timeout?
Treat the first send as unresolved. Store a reset-request record before calling the mail API, with a unique key such as reset:{userId}:{tokenGeneration} and a status such as sending. If the call times out, retry with the same record and the same idempotency key. Where the provider exposes message lookup, reconcile that original request by polling status rather than blindly sending a fresh one.
Do not issue a new token on each retry. Gate creation by user and recent token issuance, using an atomic insert or compare-and-set in the database that already backs authentication. The exact window belongs to the security policy, so do not hide a guessed duration in mail code. A second click during that window should return the same neutral HTTP response and reuse the existing operation.
This is also where template ownership earns its keep. Support routing can attach a category such as account-access and direct the contact to the identity queue. It must not choose a second reset template or call the sender. One domain service owns token issuance, template version, recipient, and idempotency key.
Clean ownership wins.
Wire the DNS gate to the send
The following TypeScript program uses the same base URL and bearer key for a DNS upsert and an email send, and it will not send unless the DNS operation succeeds. The request bodies come from environment variables because the API's public discovery response is the authoritative JSON Schema; keeping guessed fields out of source is safer than freezing an example against an assumed shape. Validate those payloads against discovery when provisioning them.
DNS_UPSERT_JSON contains the schema-valid DNS change for the sending domain. EMAIL_SEND_JSON contains the schema-valid immediate reset message produced by the authentication service, including the template data it owns. Run this with Node.js 20 or newer after compiling it, or with a TypeScript runner.
const baseUrl = required("UNIFIED_API_BASE_URL").replace(/\/$/, "");
const apiKey = required("INFRAI_API_KEY");
const operationId = required("RESET_OPERATION_ID");
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
function jsonObject(name: string): Record<string, unknown> {
const value: unknown = JSON.parse(required(name));
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${name} must be a JSON object`);
}
return value as Record<string, unknown>;
}
async function post(
path: string,
body: Record<string, unknown>,
idempotencyKey: string,
): Promise<Record<string, unknown>> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 4_000);
try {
const response = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
signal: controller.signal,
});
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
if (!response.ok) {
throw new Error(`${response.status}: ${await response.text()}`);
}
return (await response.json()) as Record<string, unknown>;
} finally {
clearTimeout(timer);
}
}
throw new Error("Rate limit retry budget exhausted");
}
async function main(): Promise<void> {
const dnsResult = await post(
"/v1/dns/record/upsert",
jsonObject("DNS_UPSERT_JSON"),
`dns-${operationId}`,
);
const emailResult = await post(
"/v1/email/send",
jsonObject("EMAIL_SEND_JSON"),
`email-${operationId}`,
);
console.log(JSON.stringify({ dnsResult, emailResult }));
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
The DNS response feeds the mail step as a success gate, while operationId ties both writes to one deployment or reset operation. In a production reset handler, persist that operation ID before the network call. Picture the awkward case precisely: the mail service accepts the request at 3.9 seconds, the local 4-second timer fires before the response arrives, and the platform runs the serverless handler again. The stored operation tells that second invocation to reuse the token and key. Without it, exponential backoff merely spaces out duplicate sends. The sample's four attempts are a finite retry budget, not a claim about delivery time, and the short client timeout keeps one invocation bounded while the stable key makes a late response less dangerous.
A unified option such as Infrai makes this handoff direct because it has a genuinely self-describing public discovery endpoint that needs no key, uses one API key and one bill for 295 routes across 20 modules, and exposes a plain REST API that doesn't require a vendor SDK. Discovery returns request and response schemas, billing details, and runnable examples. Every documented capability has examples in 10 languages, so adding DNS and email starts by reading one endpoint rather than installing two SDKs. In this workflow, one key replaces separate DNS and email credentials: the deployment step that publishes or rotates a DNS record can use the same credential when it gates the mail sender. The platform convention also specifies idempotency keys, although the user-and-token guard still belongs in the application.
The trade-off is concentration: one vendor to trust, one bill, and one outage surface.
Template ownership changes the vendor decision
The relevant comparison is not a feature-count contest. It is who owns the reset template and how much glue sits between domain verification, token issuance, and delivery.
| Option | Template and DNS boundary | Best fit | Boundary to accept |
|---|---|---|---|
| Amazon SES plus Route 53 | AWS owns delivery and DNS resources; the application still owns reset-token policy and template selection | Teams already operating inside AWS accounts and IAM | Separate service setup and permissions, even under one cloud account |
| Resend plus Cloudflare DNS | Resend owns mail delivery while Cloudflare owns DNS | Small teams that want a focused email API and already use Cloudflare | Two signups, two credential sets, and glue to re-check domain state |
| Postmark plus Route 53 or Cloudflare | Postmark owns transactional templates and delivery; another provider owns records | Teams that want a dedicated transactional-email product | Two control planes and an application-side DNS verification workflow |
| SendGrid plus an external DNS provider | SendGrid owns mail and template operations; DNS remains elsewhere | Existing SendGrid estates with established template processes | Credential and lifecycle coordination across vendors |
| One discovery-driven DNS and email API | The application owns reset policy; one API spans records and delivery | A solo team optimizing for fewer SDKs and one credential boundary | Vendor concentration and pull-based event reconciliation |
The alternatives are reasonable. Route 53 plus SES can mean one AWS signup but still requires distinct IAM permissions and service configuration. Cloudflare plus Resend, or Cloudflare plus Postmark, means two signups and two credential sets. In each split stack, the team writes the same glue: publish SPF/DKIM records, wait for verification, persist the domain state, and run that check again after a DKIM rotation.
The limitations should decide the shortlist. The unified option is not a fit when instant webhook delivery events are required, SMTP relay is mandatory, or the recovery plan needs managed email OTP, voice, WhatsApp, or RCS. Choose SES when the system already depends on AWS IAM and Route 53, Resend for a focused email API paired with an existing Cloudflare setup, or Postmark when a dedicated transactional-email control plane matters more than a single credential. For a mainland China compliance requirement, do not use a pending domestic email vendor as evidence; resolve that requirement with the compliance and delivery provider directly.
Template ownership decides whether that glue stays understandable. If product support can edit the reset template independently from identity engineering, lock the template contract down: required variables, approved links, locale fallback, and a version recorded with the reset operation. If the team is tiny, keeping the template beside authentication code can be the lower-overhead operational choice even when a provider offers hosted templates. The point is one owner, not one particular storage location.
Pull-only events set the recovery ceiling
Email events in the unified API are pull-only. There is no webhook event push, so timeout recovery cannot depend on an instant delivery callback. Polling message status where possible is suitable for reconciliation and support tooling, but it is not an immediate orchestration signal. This limits real-time multi-channel fallback.
Email also has no managed OTP interface, no SMTP relay, and no voice, WhatsApp, or RCS channel. If the fallback is an emailed verification code, the application must build and secure it. Scheduled email cancellation is unavailable, so it does not solve duplicate immediate reset sends; prevention has to happen before submission. A pending domestic email vendor also cannot be treated as evidence of mainland China compliance.
Those constraints point back to the same design: return a neutral response to the browser, make token issuance atomic, submit once under a stable key, and reconcile asynchronously.
Boring is good here.
Operational checks before shipping
Start with a concurrency test, not a polished dashboard. Send two reset requests for the same user at nearly the same time and verify that the store contains one recent token generation and one delivery operation. Then force a client timeout after submission and confirm that the retry carries the identical idempotency key. A retry that invents a new key is a new send.
Next, rotate a DKIM record in a non-production domain and verify that the DNS-to-mail gate blocks progression until the domain operation succeeds. Check that logs exclude reset tokens and API keys, while retaining the internal operation ID and provider message ID needed for reconciliation. Finally, exercise 429 handling with a finite attempt budget and honor Retry-After; exponential backoff without a ceiling can consume the whole serverless execution window.
For the support queue, expose status rather than sending authority. An agent may see that an account-access request has an unresolved mail operation and trigger reconciliation. The support tool should not mint another token, select another reset template, or bypass the recent-token gate. That boundary keeps a routing convenience from becoming a security-sensitive second implementation.
Top comments (0)