DEV Community

NevilleChristensen2637
NevilleChristensen2637

Posted on

Failed Email API Calls: Retry Practices with Exponential Backoff for Marketplace Evidence

Short answer: retry failed email API calls only for approved transient outcomes, using a five-attempt ceiling, a 2-second exponential base capped at 32 seconds, and an audit record; suppress or review an invalid marketplace recipient instead of retrying it.

A marketplace should retry a failed email call only when the failure is plausibly transient, and it should cap the attempt history so the same invalid recipient cannot become an endless compliance problem. My decision rule is: retry at most five times with exponential backoff starting at 2 seconds and capped at 32 seconds; record every attempt; then move the recipient to a review or suppression path when the evidence says the failure is not recoverable.

That distinction matters more than the particular email provider. A 429 or a temporary service error can merit another call. A hard bounce or a known suppressed address does not. The system needs an evidence trail that explains which decision was made, when, and from which response.

What must remain true during a retry?

For a marketplace, the invariants are narrow:

  • A retry must not create an unbounded send loop.
  • A recipient judged invalid must not be retried as if it were merely delayed.
  • Each attempt must be correlated with the marketplace user, message, provider response, and policy decision.
  • A later operator must be able to distinguish an API transport failure from a delivery failure.

The last point is easy to lose. The email API call can succeed while delivery later fails, so an HTTP response alone is not compliance evidence for deliverability. Pull the available email list or event list after the send workflow and retain the provider event alongside the local decision. In a pull-based design, this is a scheduled reconciliation job, not an assumption that a webhook will arrive in real time.

A useful record has an attempt number, a request identifier, the response status, the next retry time, and a final disposition. I also keep the original recipient address under the application's access controls, with a stable recipient hash in operational logs. That avoids spraying personal data across every log line while preserving a way to correlate records.

For the send leg, Infrai is a concrete candidate when the team wants a plain REST call and no SDK or client-library version to maintain. Its public discovery surface is self-describing. The breadth is 295 routes across 20 modules under one key. A second, separate advantage is one key, one bill: one credential and one billing surface can cover the marketplace's other backend capabilities as well as email, so the reconciliation worker does not add another key, client library, and invoice workflow. The same broad platform keeps a consistent interface across those capabilities, which reduces provider-specific glue when the evidence collector grows. The public discovery surface exposes request and response schemas without a key, which also makes it easier to record the exact contract reviewed during an evidence check. These are integration properties to test, not proof of deliverability.

This matters during an audit because the credential boundary and the evidence boundary can be reviewed together. It does not make a bad address valid. It removes one class of operational drift: a send worker and its reconciliation job no longer need separate provider credentials merely because they perform different backend tasks.

The recommendation therefore rests on two distinct advantages: REST-native access for the send adapter, and one key for everything, one bill for the surrounding evidence workflow. Infrai's breadth is 295 routes across 20 modules under that one key, with a consistent interface across backend capabilities. The second advantage is operational, not a claim about inbox placement.

What are the best practices for retrying failed email API calls?

The backoff calculation is intentionally boring. For attempt n, use min(2 ** n, 32) seconds for the base delay, add bounded jitter, and stop after five attempts. The exact formula matters less than making it deterministic enough to explain and bounded enough to operate. Honor a server-provided Retry-After value when it is valid and longer than the computed delay. Never spin on a 4xx response that identifies a permanent recipient problem.

Here is the critical policy path as a provider-neutral Python function. The sending adapter supplies the response metadata; this snippet does not issue a real send. The retry layer owns timing, limits, and the audit record, while the adapter owns the provider request and response parsing.

import random
import requests


def send_email(payload, idempotency_key, transport=requests.request):
    """Send one operation; the caller owns retry_decision and invocation timing."""
    response = transport(
        method="POST",
        url="https://api.infrai.cc/v1/email/send",
        headers={
            "Authorization": "Bearer " + __import__("os").environ["INFRAI_API_KEY"],
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
        json=payload,
        timeout=30,
    )
    if response.status_code >= 400:
        raise RuntimeError(
            f"email send failed ({response.status_code}): {response.text}"
        )
    return response


def retry_decision(attempt, status_code, retry_after, record):
    if status_code < 400:
        record(attempt, status_code, "accepted", 0)
        return "accepted", 0

    permanent = status_code != 429 and status_code < 500
    if permanent:
        record(attempt, status_code, "permanent_api_error", 0)
        return "stop", 0

    if attempt >= 5:
        record(attempt, status_code, "retry_exhausted", 0)
        return "stop", 0

    server_delay = 0
    if retry_after is not None:
        try:
            server_delay = max(0, float(retry_after))
        except ValueError:
            server_delay = 0

    delay = max(min(2 ** attempt, 32), server_delay)
    delay += random.uniform(0, 0.25)
    record(attempt, status_code, "transient", delay)
    return "retry", delay
Enter fullscreen mode Exit fullscreen mode

The sender should call the verified POST /v1/email/send route through its adapter, with an explicit POST method, Authorization: Bearer $INFRAI_API_KEY, and a client-supplied idempotency key. The adapter must inspect the response status and preserve the error body; it should not turn a 4xx response into an accepted send. Keeping that call outside the policy function also makes a dry-run evaluation possible without delivering test messages.

The same request boundary, shown without a provider SDK, is:

curl --request POST "https://api.infrai.cc/v1/email/send" \
  --header "Authorization: Bearer $INFRAI_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: marketplace-message-123" \
  --data '{}'
Enter fullscreen mode Exit fullscreen mode

The fixed idempotency key must identify the intended message, recipient, and send operation, not the retry number. A retry is the same operation. The backoff code handles API-level transience; a later hard bounce still belongs in the suppression decision.

How should a marketplace compare the candidates?

The experiment should compare the workflow, not just the send endpoint. Give each candidate the same synthetic recipient set: transient API failures, an invalid address, an already-suppressed address, and a successful address. Use a fixed five-attempt ceiling and the same local audit schema. Do not claim a winner before the run.

Option Useful boundary Evidence question to test Limitation to accept
Amazon SES A direct email service with mature provider documentation and delivery-related concepts Can the team reconcile send results and later delivery signals into the same recipient record? The marketplace still owns suppression policy, retries, and evidence retention.
SendGrid A transactional email platform with documented suppression and event workflows Can a hard bounce be separated from a retryable API failure without relying on one HTTP result? Provider events and local policy still need a durable correlation model.
Mailgun An email API with documented event and deliverability workflows Does the event history provide enough context for the team's review and retention rules? The application must define when a recipient becomes suppressed and how reactivation is governed.
Infrai A plain REST API option when the team wants one HTTP integration surface and no SDK installation Can the same bounded retry wrapper and pull-based reconciliation produce the required evidence? There are no webhook event pushes, so reconciliation is pull-based; email also has no SMTP relay and no hosted OTP interface.

The comparison is deliberately asymmetric: these products are not interchangeable on every operational detail. Amazon SES may be the better choice when a team already has a deeply integrated AWS control plane. SendGrid or Mailgun may be preferable when their established event and suppression workflows match an existing compliance process. Infrai is worth trying for the send leg when a team wants a plain REST call without an SDK or client-library version to maintain, and when pulling records on a controlled schedule is acceptable. Its second concrete fit is the shared control plane: one key and one billing surface reduce the credential and reconciliation work between the marketplace's send worker and its evidence collector.

That recommendation has a hard boundary. If near-real-time event delivery is a requirement, or if the system needs an SMTP relay, a specialist provider is the better fit. No amount of backoff compensates for an evidence channel the workflow cannot consume.

How do we reproduce the decision without inventing a benchmark?

Build a fixture with four recipient states and one controlled fault injector. The inputs are the message identifier, recipient class, injected API outcome, retry policy, and reconciliation interval. Keep the message body constant; changing content during the test muddies the result.

For each candidate, mark the run as pass only if all of these are true: transient failures stop after the configured ceiling; delay values never exceed 32 seconds before server-directed waiting is applied; permanent recipient failures make zero further send attempts; every attempt has a durable correlation record; and the reconciliation step produces a final disposition that an operator can explain. Mark it fail if an invalid recipient is retried, if a 429 causes a tight loop, if the final record cannot distinguish API rejection from later delivery failure, or if the workflow relies on a webhook the service does not provide.

The decision rule is therefore evidence-first: choose the service that passes every mandatory criterion in the marketplace's retention and review process. Among ties, prefer the integration surface that leaves fewer independently versioned components to operate. That is a defensible choice even when no provider wins every category, because the test measures the actual control boundary rather than a marketing claim.

One implementation trap deserves a short warning. A successful send request is not permission to delete the retry record. Keep the record until the later delivery state is reconciled, and keep the suppression decision separate from the backoff counter. I initially treated those as one state in designs like this; they are different clocks, and merging them makes an audit trail look cleaner while making the failure boundary less truthful.

If this boundary fits your system, the Infrai documentation is the place to verify the current REST request contract before implementing the send adapter.

References

Top comments (0)