# Retry safely

> Handle timeouts, rate limits, and server errors without ever sending the same email twice.

Networks fail. A request can time out after the email was queued, and retrying without protection would send it twice. Idempotency keys solve this.

## Use a stable idempotency key

Every send needs an `Idempotency-Key` header. Derive it from the event that triggers the email, and store it with that event:

| Email | Good key | Avoid |
| --- | --- | --- |
| Welcome | `welcome-user_8f3k2` | A random UUID per attempt |
| Receipt | `receipt-order_1042` | A timestamp |
| Password reset | `reset-request_77ab1` | The recipient's address alone |

Retrying with the same key and request returns the original response — `messageId`, delivery IDs, and all — without sending again. A different request with a reused key is rejected with `400`.

## Decide what to retry

| Response | Retry? | Why |
| --- | --- | --- |
| Network error or timeout | Yes | The email may or may not have been queued; the key makes the retry safe. |
| `429 Too Many Requests` | Yes, after `Retry-After` | Unless the body has a `code` for a plan limit — then retrying won't help. |
| `500`, `502`, `503`, `504` | Yes | Temporary server-side failures. |
| `400`, `401`, `403`, `404`, `409` | No | Fix the request, key, or permissions first. |

## Back off exponentially

Wait longer between attempts, add random jitter so many clients don't retry in lockstep, and cap the number of attempts.

```typescript
async function sendWithRetry(send: () => Promise<Response>, attempts = 4) {
  for (let attempt = 0; ; attempt++) {
    const response = await send().catch(() => undefined);
    const retryable = !response || response.status === 429 || response.status >= 500;

    if (!retryable || attempt === attempts - 1) {
      return response;
    }

    const retryAfter = Number(response?.headers.get('Retry-After'));
    const backoff = 250 * 2 ** attempt + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, retryAfter ? retryAfter * 1000 : backoff));
  }
}
```

The [Node.js SDK](/docs/examples/nodejs#retries-and-timeouts) does this for you, reusing the same key on every attempt.

## Send from a background job

For emails triggered by user actions, enqueue a job instead of sending inside the web request. The job stores the idempotency key, retries with backoff, and survives deploys and restarts — and your users never wait on email.

---

Source: https://connect.nxiom.com/docs/learn/retry-safely
