---
title: "Fallback and retries"
description: "Retry rate limits and fall back to a second provider, only when the first provider proved it did not take the message."
---

Get a message through when a provider rate limits you or refuses your sender, without texting anyone twice. The SDK retries or moves to another provider only when the provider proved it did not take the message.

:::warning[Unknown outcomes are never retried]
After a timeout, network error, 5xx, or malformed success response, the provider may have the message. The SDK throws `HandoffUnknownError` and does not retry or fall back. No option changes this. See [Prevent duplicate sends](/sending/idempotency#reconcile-an-unknown-outcome) to sort it out.
:::

## Fall back to a second provider

List adapters in priority order and set `fallback: "on-known-rejection"`:

```ts
import { createSmsClient } from "@opencoredev/sms-sdk";
import { telnyx } from "@opencoredev/sms-sdk/telnyx";
import { twilio } from "@opencoredev/sms-sdk/twilio";

const sms = createSmsClient({
  adapters: [
    twilio({
      accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
      authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
      from: "+15550100001",
    }),
    telnyx({ apiKey: process.env.TELNYX_API_KEY ?? "", from: "+15550100002" }),
  ],
  fallback: "on-known-rejection",
  retry: { maxAttempts: 3 },
});

const result = await sms.send({ to: "+14155550123", body: "Your code is 482913" });
console.log(result.provider, result.attemptedProviders); // "telnyx" ["twilio", "telnyx"] if Twilio refused
```

Twilio sends first. If Twilio refuses for a reason tied to that account, such as bad credentials or an unregistered sender, Telnyx sends next. Without `fallback`, only the first adapter is used.

Each adapter sends from its own `from`. With fallback, leave `from` off the message unless every adapter can send from that exact sender.

## Which rejections fall back

A rejection falls back when the problem belongs to one provider account. It stops when any provider would refuse the same message.

| Category | Falls back | Why | Examples |
| --- | --- | --- | --- |
| `auth` | Yes | Credentials are wrong for this account only. | 401, Twilio 20003, Telnyx 10009 |
| `rate_limited` | Yes, after retries on the same provider | This account is throttled. | Twilio 20429, Telnyx 10011 or 40318, Vonage 1000 or 1241 |
| `sender` | Yes | This account's sender cannot be used here. | Twilio 21606, Telnyx 40305, Vonage 1420 |
| `account` | Yes | Balance, trial, country permission, or spend limit. | Twilio 21408, Telnyx 40333, Vonage 402 |
| `recipient` | No | Another provider would reject the number too. | Twilio 21211, Telnyx 40310, Vonage 1430 |
| `compliance` | Never | The recipient opted out or the content was blocked. A second provider must not be used to get around that. | Twilio 21610, Telnyx 40300, Vonage 1240 |
| `request` | No | The request itself is wrong. | Any other 4xx |

Providers differ in how precisely their errors map. Plivo documents no error codes, so only its 401 and 429 are classified and every other 4xx is `request`. Each provider page lists its codes. In code, `error.fallbackEligible` and `isFallbackEligible(category)` give the answer.

Fallback never runs after an accepted or unknown outcome, or after `beforeSend` rejects. It skips adapters that cannot send the message's fields or sender type, but the first adapter must support them all.

When every adapter rejects, `send()` throws `AllProvidersRejectedError` with one rejection per adapter in `errors`. When the chain stops at a rejection that does not fall back, that rejection is thrown directly.

## Retries on the same provider

Only a proven rate limit (category `rate_limited`) is retried. By default each adapter gets 2 requests per send.

| `retry` option | Default | Meaning |
| --- | --- | --- |
| `maxAttempts` | `2` | Requests per adapter, counting the first. `1` turns retries off. |
| `baseDelayMs` | `500` | Base for exponential backoff with full jitter: a random wait up to `baseDelayMs * 2^(attempt - 1)`. |
| `maxDelayMs` | `10000` | Longest wait. If the provider's `Retry-After` is longer, the SDK stops retrying that adapter instead of waiting. |

A provider `Retry-After` that fits within `maxDelayMs` is honored. Aborting during the wait throws `SendAbortedError`, which is retry safe.

Two cases look retryable but are not:

- **5xx responses.** A provider can return a 5xx after it created the message, even when it says "try again later".
- **A bare 429.** A 429 counts as a rate limit only when its body carries the provider's documented rate-limit error, such as Twilio 20429. A 429 from a proxy or CDN says nothing about the message, so it is an unknown outcome.

## Before you turn fallback on

Each provider account in the chain needs:

- **Its own registered sender.** A 10DLC registration on Twilio does not cover your Telnyx number. See [Registration and consent](/providers/registration-and-consent).
- **Webhook URLs**, so status updates and replies arrive from both. See [Webhooks](/receiving/webhooks).
- **Your shared opt-out list.** Twilio records a STOP sent to your Twilio number, and Telnyx never sees it. Check your own list in `beforeSend`. See [STOP, HELP, and opt-outs](/receiving/opt-outs).

## Next steps

- [Prevent duplicate sends](/sending/idempotency)
- [Errors reference](/reference/errors)
