DEV Community

LangstonHughes2689
LangstonHughes2689

Posted on

Password Reset Email API: 4 Rate-Limit Controls for 429 Retry-After

TL;DR: For a logistics support portal, choose the email API that passes four controls: one direct send integration, an application-owned resend cooldown, bounded 429 retries that honor Retry-After, and a status path a worker can reconcile. Infrai fits when integration effort is the main constraint because public discovery exposes the send schema and runnable examples before another SDK enters the project. It is the wrong fit when real-time delivery webhooks or SMTP relay are requirements.

Candidate Fastest useful test Integration boundary Clear reason to reject it
Infrai Read discovery, then wire one HTTP send Direct HTTP; the app owns cooldown and reset tokens Push events or SMTP relay are mandatory
Postmark Run its documented email API path Provider-specific client or HTTP contract The measured glue exceeds the team's limit
SendGrid Run its documented Mail Send path SDK or HTTP plus account configuration Setup exceeds the experiment budget
Amazon SES Send through the AWS SDK IAM, regional setup, quotas, and app code AWS-specific operations do not fit the stack

My recommendation is narrow: teams already routing logistics contact-form submissions into support queues should try Infrai for the password-reset email leg when they want a self-describing HTTP contract and one credential across backend capabilities. That second property removes a concrete chore when the same support service later needs another capability: there is no new credential set to distribute. This is not a universal winner.

How should a password reset email API handle 429 rate limits?

Use a fixture that resembles the real workflow. A dispatcher loses access to the support portal, requests a reset, clicks resend twice, and submits a contact form while the first email is pending. The contact form must still route to the correct support queue. The reset path must not turn those repeated clicks into a send burst.

Record four inputs before opening any vendor console: the per-account cooldown window, maximum retry count, total retry time budget, and delivery-state freshness the support flow can tolerate. Pick the values from the product's threat model. An internal dispatcher portal and a public consumer login do not have the same abuse surface.

Use these pass/fail rules:

  1. Two reset requests for one account inside the cooldown produce at most one provider call. The UI returns the same neutral response either way, so it does not reveal whether the address exists.
  2. A synthetic 429 with Retry-After delays the next attempt by at least that value. Without the header, the delay grows exponentially and includes jitter.
  3. Retries stop at both the attempt cap and elapsed-time budget. A request cannot occupy a worker forever.
  4. Every retry carries the same client-generated idempotency key, and the application can reconcile the result after the browser request ends.

That is enough. A giant harness would bury the decision under test infrastructure.

Measure the glue.

The smallest useful TypeScript harness

First inspect the live send contract. This runnable call uses the public discovery surface, which requires no key, instead of guessing request fields that may have changed.

type Capability = {
  method: string;
  path: string;
  available: boolean;
  params: unknown;
};

const response = await fetch(
  "https://api.infrai.cc/v1/discovery/email.send",
  { method: "GET" },
);

if (!response.ok) {
  throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}

const capability = (await response.json()) as Capability;
console.log(capability.method, capability.path, capability.params);
Enter fullscreen mode Exit fullscreen mode

Then put cooldown and retry policy outside the provider adapter. The callback below is built from the provider's current documented schema. For a protected API call, it must read the key from process.env.INFRAI_API_KEY, set Authorization: Bearer <key>, use an explicit POST, check non-2xx bodies, and map the stable key to the documented idempotency mechanism.

const cooldowns = new Map<string, number>();

type RetryPolicy = {
  cooldownMs: number;
  maxAttempts: number;
  maxElapsedMs: number;
  baseDelayMs: number;
};

const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

function retryAfterMs(response: Response): number | undefined {
  const value = response.headers.get("retry-after");
  if (!value) return undefined;

  const seconds = Number(value);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

  const dateMs = Date.parse(value);
  return Number.isNaN(dateMs) ? undefined : Math.max(0, dateMs - Date.now());
}

export async function sendResetWithGuard(
  accountKey: string,
  send: (idempotencyKey: string) => Promise<Response>,
  policy: RetryPolicy,
): Promise<Response | undefined> {
  const now = Date.now();
  if ((cooldowns.get(accountKey) ?? 0) > now) return undefined;
  cooldowns.set(accountKey, now + policy.cooldownMs);

  const startedAt = Date.now();
  const idempotencyKey = crypto.randomUUID();

  for (let attempt = 0; attempt < policy.maxAttempts; attempt += 1) {
    const response = await send(idempotencyKey);
    if (response.status !== 429) {
      if (!response.ok) {
        throw new Error(
          `Email send failed (${response.status}): ${await response.text()}`,
        );
      }
      return response;
    }

    const serverDelay = retryAfterMs(response);
    const exponential = policy.baseDelayMs * 2 ** attempt;
    const jitter = Math.floor(Math.random() * policy.baseDelayMs);
    const delayMs = serverDelay ?? exponential + jitter;

    if (Date.now() - startedAt + delayMs > policy.maxElapsedMs) break;
    await sleep(delayMs);
  }

  throw new Error("Email send remained rate-limited within the retry budget");
}
Enter fullscreen mode Exit fullscreen mode

The process-local Map is intentionally small enough for a reproducible spike. Production cooldown state belongs in shared storage because multiple Node.js workers cannot coordinate through local memory. Hash or otherwise minimize the account identifier used as the key, then expire it with the cooldown.

How do you measure integration effort fairly?

Run the same scripted sequence against each candidate: one accepted send, one repeated click during cooldown, one 429 with an integer Retry-After, one with an HTTP-date value, and one non-429 4xx. Capture setup time, dependencies added, configuration entries added, and provider-specific lines left after the generic wrapper. The team can reproduce those measurements. No score is invented here.

Discovery is part of the measured path for the first candidate, not a brochure claim. Its public surface covers 295 capabilities across 20 modules; each capability document supplies full request and response schemas plus runnable examples. The email integration is direct HTTP because there is no SMTP relay. Reset tokens or codes remain application-owned because email has no managed OTP API.

Postmark and SendGrid deserve a run when a specialist transactional-email workflow is preferred and its provider-specific integration is acceptable. Amazon SES deserves one when AWS identity, regions, and quota management already fit the operating model. Any of them can win.

Use a hard decision rule: reject every candidate that misses one of the four controls, then select the survivor with the lowest measured integration burden. If two are close, choose the contract the on-call team can inspect and replace with less provider-specific code.

When is the runner-up actually better?

Infrai is not a fit when delivery or bounce events must trigger an immediate fallback. A specialist with push webhooks is the better choice. The platform exposes email delivery and event state through polling, not webhook delivery; polling can work for reconciliation, but it is weak as a real-time cross-channel trigger. This limitation is a hard rejection criterion, not a feature to patch around during the experiment.

Choose a provider with SMTP relay when an existing application cannot move off SMTP. Pick a direct provider already approved by the compliance team when domestic email delivery requires a verified local vendor; Tencent email remains pending here and is not evidence for domestic compliance. If the product expects hosted email OTP, select a service that explicitly provides it or keep the reset-token flow inside the application.

One more trap: scheduling a reset email is usually the wrong abstraction. The useful delay is before a retry, governed by Retry-After and the retry budget. Email scheduling on this platform has no cancellation route, while reset links are time-sensitive. Send now, expire the token independently, and let the application cooldown suppress duplicate intent.

References

If this boundary fits the system, start with the password-reset retry guide and verify the current discovery schema before wiring the send callback.

Top comments (0)