> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: cooldown and rate-limit 429s

> Understand what a 429 means on the Verify and messaging APIs — recipient cooldowns, tenant frequency caps, and throughput limits — and how to retry correctly using the retry_after hint.

# Troubleshooting: cooldown and rate-limit 429s

A `429` response means the request was valid but dropped by a protective limit — a cooldown, a frequency cap, or a throughput ceiling. Every 429 carries an error code that tells you which limiter fired, and most include a `retry_after` value in seconds. For the full taxonomy of Orbit's limiter families, see [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy). This page is the live-fix surface — use the codes below to branch on which limiter is firing and how to stop hammering the same gate.

## Common Verify / OTP 429 codes

| Code                            | What fired                                                                                                                                    | What to do                                                                                                |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `VERIFY_RESEND_COOLDOWN`        | A recipient asked for a new OTP before the per-recipient resend cooldown elapsed                                                              | Hold off re-sends for that recipient until the cooldown clears; surface a countdown in your UI            |
| `VERIFY_RECIPIENT_RATE_LIMITED` | The recipient hit the per-recipient send-rate limit (anti-SMS-pumping / anti-harassment gate)                                                 | Back off sends to that number; repeated hits usually mean a loop in your client or abuse of the recipient |
| `RATE_LIMIT_EXCEEDED`           | A per-recipient, per-tenant, or per-resource frequency-cap limiter fired (verify, messaging, and several other surfaces use this shared code) | Respect the `retry_after` value in the response and retry after it elapses                                |

## Cross-channel 429 codes

The messaging and voice surfaces raise more specific 429 codes. The table below is the quick breakdown; the full catalogue with response shapes is in the [Error codes reference](/reference/error-codes), and the [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy) concept groups them into the four limiter families.

| Code                             | Family                                         | What to do                                                                           |
| -------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| `FREQUENCY_CAP_EXCEEDED`         | Frequency caps (a rule you created)            | Skip the capped recipient this window, or adjust the cap                             |
| `MESSAGING_SERVICE_MPS_EXCEEDED` | Tenant throughput (per messaging service)      | `Retry-After: 1` — back off one second                                               |
| `NUMBER_MPS_EXCEEDED`            | Tenant throughput (per number)                 | `Retry-After: 1` — back off one second                                               |
| `WHATSAPP_TIER_LIMIT_EXCEEDED`   | Channel-specific cap (Meta daily tier)         | Slow sustainably — this resets on a daily-recipient clock, not a one-second throttle |
| `CONCURRENCY_LIMIT_EXCEEDED`     | Tenant throughput (per-org voice concurrency)  | Reduce concurrent calls at the origin, or change the org concurrency cap             |
| `VOICE_COUNTRY_RATE_LIMITED`     | Tenant throughput (per-country sliding window) | Spread outbound to that country; the guard slides rather than hard-blocks            |

## Response shape

Every 429 uses the same envelope. `retry_after` lives **inside `error`**, not at the top level, and the HTTP `Retry-After` header carries the same value so proxies and SDK header-readers agree:

```json theme={null}
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry in 12 seconds.",
    "status": 429,
    "retry_after": 12
  },
  "meta": {
    "request_id": "req_01J9X4KZT8Q2M7W3N5VSP8DH6B",
    "timestamp": "2026-08-25T14:03:11.482Z",
    "docs_url": "https://docs.orbit.devotel.io/errors/RATE_LIMITED"
  }
}
```

Read `error.retry_after` (numeric seconds) when it is present; fall back to the `Retry-After` header only when the body field is missing. The global HTTP quota limiter always sets both. Per-recipient cooldown codes (`VERIFY_RESEND_COOLDOWN`, `FREQUENCY_CAP_EXCEEDED`) may carry the hint in `error.details.retry_after_seconds` instead — treat the field as a hint, not a guarantee, and fall back to backoff when it is absent.

## Codes by surface

Same codes, organized by which surface raised them. The **surface** column is the diagnostic lever: it tells you whether the limiter is keyed on one recipient, a cap you configured, or your tenant's aggregate throughput.

| Code                             | Surface                             | Retry guidance                                                                             |
| -------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ |
| `VERIFY_RESEND_COOLDOWN`         | Verify (per-recipient cooldown)     | Do not retry the same recipient until the cooldown clears; other recipients are unaffected |
| `VERIFY_RECIPIENT_RATE_LIMITED`  | Verify (per-recipient send rate)    | Back off the recipient; a retry loop on the same number never clears the gate              |
| `RATE_LIMIT_EXCEEDED`            | Verify or shared fallback           | Wait `retry_after`, then retry once                                                        |
| `FREQUENCY_CAP_EXCEEDED`         | Messaging (your frequency cap)      | Skip the contact for the current window; retries inside the window always fail             |
| `MESSAGING_SERVICE_MPS_EXCEEDED` | Messaging (per-service throughput)  | Back off \~1s; throttle your sender loop                                                   |
| `NUMBER_MPS_EXCEEDED`            | Messaging (per-number throughput)   | Back off \~1s; spread sends across your numbers                                            |
| `WHATSAPP_TIER_LIMIT_EXCEEDED`   | Messaging (Meta tier)               | Slow down for hours, not seconds — the tier resets on a daily-recipient clock              |
| `RATE_LIMIT_EXCEEDED`            | Messaging or shared fallback        | Wait `retry_after`, then retry once                                                        |
| `CONCURRENCY_LIMIT_EXCEEDED`     | Voice (org concurrency)             | Reduce concurrent calls at the origin; retry as a slot frees                               |
| `VOICE_COUNTRY_RATE_LIMITED`     | Voice (per-country window)          | Spread outbound to that country over time; the guard slides, it does not hard-block        |
| `RATE_LIMITED`                   | Flow executions / global HTTP quota | Wait `retry_after` (or the `Retry-After` header), then retry with backoff                  |

## How to retry correctly

1. **Read `retry_after`.** Most 429 responses include a `retry_after` value (seconds) — wait that long before retrying rather than looping with a fixed delay.
2. **Back off per recipient, not per batch.** Per-recipient cooldowns fire on one recipient; other recipients in the same batch are unaffected, so retry only the limited rows.
3. **Fix the loop.** If one recipient keeps tripping `VERIFY_RECIPIENT_RATE_LIMITED`, the throttle is doing its job — check for a re-send loop in your client before bumping limits.

## Backoff code

One copy-pasteable pattern covers every 429 here. It prefers the server's `retry_after` hint, falls back to exponential backoff with jitter (`2^attempt` seconds plus up to 1s random, capped at 60s), and gives up after five retries so a persistent gate cannot drain your queue:

```js theme={null}
const MAX_RETRIES = 5;
const MAX_WAIT_SECONDS = 60;

async function sendWithBackoff(url, options) {
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;

    const header = res.headers.get("retry-after");
    let body = null;
    try {
      body = await res.clone().json();
    } catch {
      // Non-JSON body — fall back to header or computed backoff
    }

    const bodyHint = body?.error?.retry_after;
    const headerHint = header !== null ? Number(header) : NaN;
    let waitSeconds =
      typeof bodyHint === "number" && bodyHint > 0
        ? bodyHint
        : Number.isFinite(headerHint) && headerHint > 0
          ? headerHint
          : Math.pow(2, attempt) + Math.random(); // 2^n s + 0–1s jitter
    waitSeconds = Math.min(waitSeconds, MAX_WAIT_SECONDS);

    if (attempt === MAX_RETRIES) {
      throw new Error(
        `Retry budget exhausted after ${MAX_RETRIES + 1} attempts (last code: ${
          body?.error?.code ?? "unknown"
        })`
      );
    }
    await new Promise((r) => setTimeout(r, waitSeconds * 1000));
  }
}
```

Two rules the snippet encodes: the **cap** (60s) stops a pathological `retry_after` from parking a worker, and the **budget** (5 retries) turns a terminal gate into an exception you can dead-letter instead of an infinite loop. Audit the thrown error code before re-queueing — per [Do not retry](#do-not-retry), some gates should discard the row.

## Identify which limiter fired

Work top-down; the first match tells you where to look next:

1. **Read `error.code` on the 429.** It names the limiter family — do not guess from the endpoint you called.
2. **Starts with `VERIFY_`?** A per-recipient gate fired. Cooldown or recipient-rate, keyed on that one phone number. See [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy) for how the recipient gates stack.
3. **Starts with `FREQUENCY_`?** A cap your org configured fired. Open the cap and check its window — see [Frequency caps](/guides/frequency-caps).
4. **Ends in `_MPS_EXCEEDED`, or is `CONCURRENCY_LIMIT_EXCEEDED` / `VOICE_COUNTRY_RATE_LIMITED`?** A tenant throughput ceiling fired. Your org is sending faster than its quota, not one recipient. Dashboard surfaces and quota tuning are in [API key usage limits](/guides/api-key-usage-limits).
5. **Is `RATE_LIMITED` or `RATE_LIMIT_EXCEEDED`?** The shared fallback or the global HTTP quota. Treat it as tenant-wide throughput and follow step 4 unless you know the recipient-specific path raised it.

## Do not retry

Two codes look retriable but are not — a retry loop makes both worse:

* **`VERIFY_RESEND_COOLDOWN`** is keyed on the recipient. Retrying the same phone number just re-trips the gate and counts against its send-rate budget. Drop that recipient from the batch, wait out the cooldown, and surface the countdown to the end user.
* **`FREQUENCY_CAP_EXCEEDED`** means the contact already received the allowed number of sends inside the rolling window. Retrying the same contact inside the window always fails. Skip the contact for this window; if legitimate traffic keeps hitting the cap, raise the cap itself in [Frequency caps](/guides/frequency-caps).

Everything else in the tables above is retriable once you wait the hint.

## Fleet-wide 429s

If every request across recipients and channels returns 429 at the same moment, you are hitting a tenant-level throughput ceiling, not a recipient gate — check your org's usage-limit dashboard, covered in [API key usage limits](/guides/api-key-usage-limits). A wallet run dry is a different failure class: those responses come back as balance errors, not 429s — see [Troubleshooting: insufficient balance](/troubleshooting/insufficient-balance). When a fleet-wide 429 spike coincides with elevated error rates across customers, check the [status page](https://status.orbit.devotel.io) before tuning your own thresholds.

<Note>
  A cooldown is not an outage. If every request across recipients returns 429 at the same moment, check for a tenant-level throughput cap; if it is only one recipient, the per-recipient gate is working as intended.
</Note>
