Skip to content
Browse docs
Open in ChatGPTView as Markdown

#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 and a Retry-After header. Retry-After is the number of seconds to wait before retrying.

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"
  }
}
  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.

#Retry-on-429 loop

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

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));
}
}

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

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 — every status code and the error object shape
  • Opt-out handling — staying compliant with carrier rules
  • Webhooks — delivery receipts so you know what actually sent