Skip to content
Convor
Convor Docs
API

Rate Limits

Read Convor rate-limit response headers and implement bounded retries without relying on obsolete plan-wide request tables.

Convor applies shared and route-specific limits to protect tenant data and platform availability. The effective ceiling can vary by route group, endpoint, organization plan, and caller type.

Do not hardcode one plan-wide requests-per-minute table. The response headers for the request you are making are the source of truth.

Response headers

Rate-limited API routes return:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1785772860000
HeaderMeaning
X-RateLimit-LimitEffective request ceiling for the current bucket and window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix timestamp in milliseconds when the current window resets.

When a request is rejected with 429 Too Many Requests, the response also contains:

Retry-After: 12

Retry-After is the number of seconds to wait before retrying. Prefer it over a locally calculated delay.

Error body

A rate-limit rejection uses the normal API error envelope:

{
  "error": {
    "code": "TOO_MANY_REQUESTS",
    "message": "Too many requests. Please try again later.",
    "details": {
      "retryAfter": 12
    },
    "correlationId": "01J..."
  }
}

The details and correlationId fields are optional. Use the HTTP header as the retry source even when the body also includes retryAfter.

Retry strategy

Use bounded retries, respect Retry-After, and add jitter so many workers do not retry at the same instant.

function sleep(milliseconds) {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

async function convorRequest(url, options, maxRetries = 4) {
  for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
    const response = await fetch(url, options);

    if (response.status !== 429 || attempt === maxRetries) {
      return response;
    }

    const retryAfter = Number(response.headers.get("Retry-After"));
    const fallbackSeconds = Math.min(30, 2 ** attempt);
    const baseDelay = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : fallbackSeconds * 1000;
    const jitter = Math.floor(Math.random() * 250);

    await sleep(baseDelay + jitter);
  }

  throw new Error("Unreachable");
}

Do not blindly retry mutations

A 429 response means the rejected request was not accepted by the rate-limit guard, but other network failures can occur after a mutation is processed. For POST, PUT, PATCH, and DELETE, follow the endpoint's replay guarantees or verify resource state before retrying.

Operational guidance

  • Pace bulk imports instead of sending the entire dataset concurrently.
  • Use a small worker pool and reduce concurrency after repeated 429 responses.
  • Cache stable reads where appropriate.
  • Prefer webhooks for change notifications instead of aggressive polling.
  • Record the endpoint, status, rate-limit headers, and correlation ID for troubleshooting, but never log the API key.

Was this page helpful?

On this page