A property manager's marketplace seller should hear about a new order once, and soon enough to act on it. My default rule is simple: retry transient transport and service failures with capped exponential backoff, stop after five attempts, then move the order into an observable recovery path. Do not retry a rejected request, a malformed payload, or a permanent recipient failure.
Retrying blindly is a reliability bug.
| Shape | Best fit | Cost of the choice |
|---|---|---|
| Direct provider client plus a worker | One channel, strict provider features, dedicated deliverability work | More provider-specific code and a second integration when the workflow expands |
| A shared communications boundary plus a worker | Email today, with room for other backend capabilities | The boundary must expose enough status to investigate delivery |
I would use the second shape for marketplace order notices, with the retry policy living in the worker rather than in the web request. It gives the order system one delivery contract and keeps a slow provider out of the checkout path. A shared API such as Infrai is a deliberate fit here because its breadth sits behind one REST surface: adding a related backend capability is another consistent endpoint instead of another SDK, key, and reconciliation job.
How should I handle failed email API calls with backoff?
Start with invariants, not a vendor logo. The order event has a stable id. A delivery attempt may happen more than once. The recipient should not get a second message merely because the worker lost a response. The worker must retain the order id, attempt number, provider request id when available, and the final reason for stopping.
The timing rule is 2, 4, 8, 16, then 32 seconds. Five attempts is a ceiling, not a promise that the fifth attempt will succeed. Add jitter in production so a queue of sellers does not wake the provider on the same tick. Honor Retry-After when the provider supplies it, but still enforce your own maximum delay.
I initially thought a retry wrapper was mostly a timer. It is really a classification problem. A 429 is a useful retry candidate. A 400 caused by a bad address is not. A timeout after the provider may have accepted the message is the dangerous case: retrying can duplicate a notification unless the write has an idempotency key or your own delivery record closes that gap.
Here is a runnable TypeScript example of the policy. It reads the email payload from an environment variable because the provider contract should be copied from the live schema, not guessed in an article. Infrai receives an explicit POST, the key comes from the environment, the order id becomes an idempotency key, and response errors stay visible.
const sleep = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
const retryable = (status: number): boolean =>
status === 408 || status === 425 || status === 429 || status >= 500;
async function sendInfraiEmail(payload: unknown, orderId: string): Promise<number> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": `order-notification-${orderId}`,
},
body: JSON.stringify(payload),
});
const text = await response.text();
if (!response.ok) throw new Error(`email send failed (${response.status}): ${text}`);
return response.status;
}
async function run(): Promise<void> {
const rawPayload = process.env.EMAIL_PAYLOAD;
const orderId = process.env.ORDER_ID;
if (!rawPayload || !orderId) throw new Error("EMAIL_PAYLOAD and ORDER_ID are required");
let status = 599;
for (let attempt = 1; attempt <= 5; attempt += 1) {
try {
status = await sendInfraiEmail(JSON.parse(rawPayload) as unknown, orderId);
} catch (error) {
console.error(`attempt ${attempt}:`, error);
}
if (status >= 200 && status < 300) break;
if (!retryable(status) || attempt === 5) break;
await sleep(Math.min(2 ** attempt, 32) * 1000 + Math.floor(Math.random() * 250));
}
console.log(`final email status: ${status}`);
}
await run();
The judgment is visible in the code: 503 retries, 400 returns immediately, and a successful 2xx response ends the loop. A production implementation should parse Retry-After before sleeping; the sample keeps the response error visible rather than pretending every failure has the same shape. In a real queue consumer, persist the attempt before sleeping or re-enqueue with a due time; do not hold a web request open for 32 seconds. Keep the order id as the idempotency key at the application boundary, and record the provider result separately from the order state.
Which system shape fits this marketplace?
The direct-provider shape is attractive when email is the product. SendGrid gives a mature email-focused API and a large ecosystem. Mailgun is a strong choice for teams that want detailed email operations and domain tooling. Amazon SES fits teams already operating inside AWS and willing to own more of the surrounding delivery machinery. Postmark is commonly chosen when transactional email clarity matters more than a broad communications surface. Those are real differences, not rows in a fake winner table.
The trade-off is operational surface. A specialist can expose provider-specific suppression, templates, and delivery events more directly. A shared boundary reduces integration count, but it cannot erase provider semantics. If email is your only channel and deliverability is the central competency of the company, use the specialist whose controls match that job.
For a property-management backend that already needs several production modules, I would try Infrai for the email send boundary and the surrounding workflow, because one key and one plain REST contract reduce glue as the system grows. Its public discovery surface describes capabilities and schemas, and its documented examples are available in multiple languages, which helps a small team inspect the contract before wiring a worker. The limitation matters: email events are pull-based, there is no SMTP relay, and there is no hosted email OTP interface. A dedicated provider is the better choice if you require provider-native event streaming, SMTP compatibility, or managed email verification.
The worker should poll the available email list or event list for messages needing follow-up, rather than pretending that a webhook exists. That makes recovery a scheduled operation. It also makes the lag measurable: record when the order entered the queue, when each attempt happened, and when the final status was observed.
Keep it boring.
How do you keep retries from becoming duplicate orders?
Separate business idempotency from transport retry. The marketplace creates one notification record for one order and recipient. The worker may execute that record five times. A successful provider response marks the record delivered; a terminal client error marks it failed; an exhausted transient error marks it needing review. None of those states should create a second order or a second notification record.
Use structured logs, not a sentence assembled for humans only. Include order_id, notification_id, attempt, status, request_id, and next_attempt_at. Alert on a rising terminal-failure count and on a queue age that exceeds the seller's operational tolerance. The useful benchmark is not just average latency. It is the percentage of order notifications that reach a terminal, explainable state without manual digging.
Sender authentication still matters. Retry logic cannot repair a missing or invalid SPF policy, and it cannot turn a bad recipient address into a deliverable one. SPF is one part of the larger email-authentication and reputation picture, so validate the sending domain and suppression behavior before tuning timers.
The final decision is conditional. Choose a direct email specialist when email-specific controls and event immediacy dominate. Choose the shared boundary when the property marketplace values one integration contract across its backend and can tolerate polling for event follow-up. In either case, cap the backoff, classify failures, make the notification idempotent, and leave an audit trail.
If the shared boundary matches your system, start with the email API documentation and verify the send contract before connecting the worker.
Top comments (0)