Docs
Docs/API Reference/Rate Limits

Rate Limits

Octopost API rate limits: requests per minute, the monthly X allowance on each plan, what a 429 response looks like, and how to back off and retry.

Request limits#

The same limits apply on every plan:

BucketLimitApplies to
Default100 requests per minuteEvery /api/* request not listed below.
Publish50 requests per hourPOST /api/posts/{id}/publish. These requests don't count toward the default bucket.
Account connect20 per hourRequests to /api/accounts/connect/* and /api/accounts/oauth/* (starting a connection).

Limits are counted per client IP address, not per API key, and each bucket uses a fixed window that starts with your first request. Creating a post with published: true counts toward the default bucket, not the publish bucket.

The MCP endpoint at https://octopost.ink/mcp isn't covered by these limits.

The monthly X allowance#

X charges per post, so every plan includes a monthly X allowance counted in units:

PlanX units per month
Basic500
Premium1,500
Business4,000
Trial40

A post to X uses 1 unit, or 13 units if it contains a link. When the allowance is used up, X shows up as a failed network in the publish results ("errorCode": "PLAN_LIMIT", with the used and total units in error). The request itself still succeeds, and the other networks still publish.

429 Too Many Requests#

Going over a request limit returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1762268460
{ "error": "Too many requests. Please try again later." }
HeaderMeaning
Retry-AfterSeconds until the window resets.
X-RateLimit-RemainingAlways 0 on a 429.
X-RateLimit-ResetUnix time (seconds) when the window resets.

These headers are only sent on 429 responses. Successful responses don't include rate limit headers, so keep your own count if you need to pace requests.

Backing off#

  1. Wait the number of seconds in Retry-After.
  2. If it's missing, back off exponentially (1 s, 2 s, 4 s, up to 60 s) and add a little random jitter.
async function fetchWithBackoff(url: string, options: RequestInit, maxRetries = 5): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
    if (response.status !== 429) return response;
    if (attempt === maxRetries) throw new Error("Rate limited after maximum retries");

    const retryAfter = response.headers.get("Retry-After");
    const delaySeconds = retryAfter ? parseInt(retryAfter, 10) : Math.min(2 ** attempt, 60);
    await new Promise((r) => setTimeout(r, (delaySeconds + Math.random()) * 1000));
  }
  throw new Error("Unreachable");
}

Tips#

  • Use webhooks to hear about scheduled posts instead of polling GET /api/posts/{id}.
  • GET /api/posts returns every post at once, so one call is usually enough.
  • Cache account and preset lists. They rarely change.

Related guides