> ## 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.

## Note de langue

Lorsqu’une traduction n’est pas disponible, le contenu anglais est affiché comme solution de repli. Conservez les codes d’erreur, les chemins d’API et les blocs de code.

# 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 |
| `DAILY_CAP_EXCEEDED` / `WARMING_QUOTA_EXCEEDED` | Sender warming (per-number daily ramp) | Not a throughput limit — the number's warming ceiling for the UTC day is spent; see [Troubleshooting: number warming caps](/troubleshooting/number-warming-caps) |
| `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 |
| `VOICE_TRUNK_HOURLY_CAP` | Per-trunk quota (customer SIP trunk) | Not tenant-wide — this trunk's own configured hourly budget; see [Troubleshooting: SIP trunk capacity gates](/troubleshooting/sip-trunk#capacity-gates) |
| `VOICE_TRUNK_CPS_CEILING` | Per-trunk quota (customer SIP trunk) | Slow the trunk's dialer under the fixed 50-calls/second ceiling; see the [SIP-trunk page](/troubleshooting/sip-trunk#capacity-gates) |

## 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 |
| `VOICE_TRUNK_HOURLY_CAP` / `VOICE_TRUNK_CPS_CEILING` | Voice (per-trunk quota on your own SIP trunk) | The trunk's own hourly budget or burst ceiling — retriable, but check the trunk first; see the [SIP-trunk capacity gates](/troubleshooting/sip-trunk#capacity-gates) |
| `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. **Starts with `VOICE_TRUNK_`?** A per-trunk quota on one of your own SIP trunks fired — hourly budget or burst ceiling (the concurrent-cap variant answers at SIP level as `486`, so it rarely returns an HTTP 429). Read the trunk's gauge and raise or bypass the cap per [SIP-trunk capacity gates](/troubleshooting/sip-trunk#capacity-gates).
6. **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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.