<!-- VanceTel docs · /docs/rate-limits -->

# Rate limits

Requests are rate limited **per API key**. When you exceed your limit, the API returns `429 Too Many Requests` with a `Retry-After` header telling you how long to wait. Build a small backoff loop and you will rarely notice the ceiling.

## How limits work

- Limits are applied **per API key**, so test and live keys are metered independently.
- Limits are measured as a request rate (requests per second / per minute) with short bursts allowed above the steady rate.
- When you go over, further requests return `429` until the window refreshes.

  Indicative steady-state limits are on the order of a few requests per second per key, with
  short bursts allowed. These figures are **indicative, not guarantees** — your account's
  effective limits depend on your plan and traffic history. Design for backoff rather than a
  fixed number.

## The 429 response

A throttled request returns `429` with the standard [error object](/developers/docs/errors) and a `Retry-After` header. `Retry-After` is the number of seconds to wait before retrying.

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{
  "error": {
    "type": "rate_limited",
    "message": "too many requests — retry after 2 seconds"
  }
}
```

## Recommended client behavior

1. **Respect `Retry-After` first.** When present, wait at least that many seconds before retrying.
2. **Fall back to exponential backoff** when `Retry-After` is absent (e.g. on a `5xx`): wait 1s, 2s, 4s, 8s, doubling each attempt.
3. **Add jitter** — a small random offset on each delay so concurrent clients do not retry in lockstep.
4. **Cap retries** — give up after a few attempts and surface the failure rather than looping forever.
5. **Smooth your own send rate** — spread bulk sends over time instead of firing them all at once.

  Never retry a `4xx` other than `429`. A `400`, `401`, `404`, or `422` will keep failing until
  you fix the request — see [Errors](/developers/docs/errors).

## Retry-on-429 loop

This loop retries on `429` and `5xx`, honors `Retry-After` when present, and otherwise backs off exponentially with jitter.

**Node**

```ts
async function sendWithRetry(payload, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch("https://api.netexem.com/v1/messages", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NETEXEM_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (res.ok) return res.json();

    // Only retry on rate limits and server errors.
    if (res.status !== 429 && res.status < 500) {
      const { error } = await res.json();
      throw new Error(`${res.status} ${error.type}: ${error.message}`);
    }

    if (attempt === maxRetries) {
      throw new Error(`Gave up after ${maxRetries} retries (last status ${res.status})`);
    }

    // Honor Retry-After, else exponential backoff with jitter.
    const retryAfter = Number(res.headers.get("Retry-After"));
    const backoff = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : 2 ** attempt * 1000;
    const jitter = Math.random() * 250;
    await new Promise((r) => setTimeout(r, backoff + jitter));
  }
}
```

**Python**

```python
import os, time, random, requests

def send_with_retry(payload, max_retries=5):
    for attempt in range(max_retries + 1):
        res = requests.post(
            "https://api.netexem.com/v1/messages",
            headers={"Authorization": f"Bearer {os.environ['NETEXEM_API_KEY']}"},
            json=payload,
        )

        if res.ok:
            return res.json()

        # Only retry on rate limits and server errors.
        if res.status_code != 429 and res.status_code < 500:
            error = res.json()["error"]
            raise RuntimeError(f"{res.status_code} {error['type']}: {error['message']}")

        if attempt == max_retries:
            raise RuntimeError(
                f"Gave up after {max_retries} retries (last status {res.status_code})"
            )

        # Honor Retry-After, else exponential backoff with jitter.
        retry_after = res.headers.get("Retry-After")
        backoff = float(retry_after) if retry_after else 2 ** attempt
        time.sleep(backoff + random.uniform(0, 0.25))
```

## Carrier and A2P limits

Request-rate limits are not the only ceiling on messaging. Outbound SMS/MMS throughput is also bounded by **carrier and A2P (application-to-business) limits** — per-number sending rates and registered campaign throughput set by the carriers, independent of the API.

- These limits apply even when you are well under your API request rate.
- They protect deliverability and keep your numbers in good standing.
- Honoring opt-outs is part of staying within them — see [Opt-out handling](/developers/docs/opt-out).

  For high-volume sending, queue messages on your side and drain the queue at a steady rate.
  This keeps you under both the API request limit and carrier throughput limits, and makes
  bursts easy to absorb.

## Next steps

- [Errors](/developers/docs/errors) — every status code and the error object shape
- [Opt-out handling](/developers/docs/opt-out) — staying compliant with carrier rules
- [Webhooks](/developers/docs/webhooks) — delivery receipts so you know what actually sent
