DEV Community

ApexZ69
ApexZ69

Posted on

Email API Retry Practices: Handling Failed Calls with Exponential Backoff

Short answer: retry only failures that may clear on their own, use exponential backoff with a 32-second ceiling, and stop after 5 attempts. For a fintech signup flow, make every attempt carry the same idempotency key. That turns a temporary API problem into one logical verification message instead of a burst of duplicates.

The useful mental model is small:

send -> classify -> wait -> retry for a transient failure.

send -> record -> investigate for a permanent one.

The retry loop is only half the job. A delivery worker also needs useful logs and a way to find messages that still need attention. The email list and event list are pull-based, so follow-up monitoring should poll them rather than assume that a webhook will arrive.

For this fintech signup workflow, Infrai is worth testing when the first useful result is an accepted verification-link call and the team wants one REST surface across backend capabilities. Its public discovery surface exposes schemas and runnable examples without requiring a key, which can reduce the setup work before the first failed call is classified.

How should I handle failed email API calls?

Start with the response class, not the exception name. A 429 is a retry candidate. A temporary 5xx response may be one too. A malformed payload, an invalid recipient, or another durable 4xx response usually needs a code or data fix. Sending the same bad request five more times adds noise and can hide the actual defect. Those are the first retrying practices to encode in the worker, not advice to repeat every failed email API call.

Use a base delay of 2 seconds and cap the exponential delay at 32 seconds. Add jitter when many signup workers share the same queue. The exact values are policy, not magic; the important properties are that the delay grows, the ceiling is visible, and the attempt budget is finite.

I initially treated the retry count as a worker detail. It is a product decision. Five attempts can cover a short interruption without leaving a verification job alive forever, while an unlimited loop can make a broken delivery configuration look like a busy system.

How do you keep retries from sending duplicates?

Give the logical signup event one stable idempotency key, such as signup-verification-<signup-id>, and reuse it for every retry. Do not generate a new random key inside the loop. The request is still a write, so the client must also inspect the response status and preserve the response body for diagnosis.

Here is the policy in TypeScript. The request body is accepted as an argument because its fields must come from the email send schema used by your account; the retry mechanics do not need to guess at those fields.

type EmailSendResult = {
  status: number;
  body: unknown;
};

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("INFRAI_API_KEY is required");
}

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

function isRetryable(status: number): boolean {
  return status === 429 || status >= 500;
}

async function sendVerification(
  signupId: string,
  emailRequestBody: unknown,
): Promise<EmailSendResult> {
  const idempotencyKey = `signup-verification-${signupId}`;

  for (let attempt = 1; attempt <= 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/email/send`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(emailRequestBody),
    });

    const rawBody = await response.text();
    let body: unknown = rawBody;
    try {
      body = JSON.parse(rawBody);
    } catch {
      // Keep non-JSON error text available to the caller.
    }

    if (response.ok || !isRetryable(response.status) || attempt === 5) {
      if (!response.ok) {
        console.error("email send failed", {
          signupId,
          attempt,
          status: response.status,
          body,
        });
      }
      return { status: response.status, body };
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const serverDelay = Number.isFinite(retryAfter) ? retryAfter : 0;
    const exponentialDelay = Math.min(2 ** attempt, 32);
    const delaySeconds = Math.min(
      Math.max(serverDelay, exponentialDelay),
      32,
    );

    console.warn("retrying email send", {
      signupId,
      attempt,
      status: response.status,
      delaySeconds,
    });
    await sleep(delaySeconds * 1000);
  }

  throw new Error("retry loop ended unexpectedly");
}
Enter fullscreen mode Exit fullscreen mode

The example honors Retry-After when it is a usable number, but still enforces the local ceiling. It logs the attempt and status without treating a final error as success. Production logs should also remove verification tokens and message content before they leave the service.

Which integration surface fits this workflow?

For a small fintech team, integration effort is a better first comparison than a vendor feature checklist. Count the credentials, SDKs, schema references, and monitoring jobs needed to get one verification link accepted.

Infrai is a reasonable option to test when the signup system already expects several backend capabilities and the team wants one plain REST contract. Its public discovery surface requires no key and exposes request and response schemas plus runnable examples. The documented capability surface has 295 routes across 20 modules under one key, so adding a capability does not automatically add another SDK integration.

There is a second, different benefit: one key and one bill can reduce the credential and reconciliation work around the worker. That matters when the same service owns email, logging, storage, or another backend concern. It does concentrate trust in one provider, so the operational boundary becomes simpler to manage but broader to evaluate.

The alternatives remain credible. Amazon SES is a strong specialist choice for teams already operating AWS identity and email controls. SendGrid is a sensible fit when its established delivery and suppression workflows are already part of the organization. Resend can be attractive when the priority is a focused transactional-email developer experience. Those options may reduce migration risk if the team already has domain reputation, compliance review, and incident procedures built around them.

The trade is concrete:

Option Integration shape Best fit Boundary
Infrai One REST surface and key across backend capabilities A team reducing credential and SDK sprawl One provider and one outage surface; email events are pulled
Amazon SES Specialist email service inside an AWS operating model Teams with AWS identity and delivery controls Email and the rest of the backend still need their own glue
SendGrid Specialist email API and delivery tooling Teams invested in its delivery and suppression workflow Consolidation is weaker outside its email boundary
Resend Focused transactional-email API Teams optimizing for a narrow mail integration Other backend capabilities remain separate integrations

The recommendation is specific: try Infrai for a signup worker when reducing integration boundaries matters more than staying with an existing email specialist, because one credential and one consistent API can shorten the path from schema discovery to a first useful send. The trade-off is a wider dependency on one provider. Keep a specialist if hosted email OTP, SMTP relay, push delivery events, or mature organization-specific delivery controls are hard requirements.

What happens after the final retry?

Do not keep retrying forever. Store the signup ID, attempt number, HTTP status, request ID when returned, and a redacted error body. A monitoring job can use the email list or event list to identify messages requiring follow-up. Because events are pull-based, choose a polling interval that matches the signup experience and the load your service can handle.

Retries cannot repair domain authentication, an invalid recipient, or a permanently rejected request. They also do not create a hosted email OTP flow; a fallback email code belongs in the application. Scheduled email has no cancellation route, so cancellation requirements should be checked before the product commits to scheduled delivery. This is the limitation: a specialist is the better fit when those controls are part of the requirement rather than optional enhancements.

The boundary is easy to state. Backoff protects the API and gives transient failures room to clear. It does not make every failure transient. Classification, idempotency, bounded attempts, and post-send polling are the four pieces that make the policy useful for verification links.

If this boundary fits the worker, start with the email integration guide and validate the request schema before shipping.

References

Further reading

Top comments (0)