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:
| Bucket | Limit | Applies to |
|---|---|---|
| Default | 100 requests per minute | Every /api/* request not listed below. |
| Publish | 50 requests per hour | POST /api/posts/{id}/publish. These requests don't count toward the default bucket. |
| Account connect | 20 per hour | Requests 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:
| Plan | X units per month |
|---|---|
| Basic | 500 |
| Premium | 1,500 |
| Business | 4,000 |
| Trial | 40 |
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." }| Header | Meaning |
|---|---|
Retry-After | Seconds until the window resets. |
X-RateLimit-Remaining | Always 0 on a 429. |
X-RateLimit-Reset | Unix 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#
- Wait the number of seconds in
Retry-After. - 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/postsreturns every post at once, so one call is usually enough.- Cache account and preset lists. They rarely change.