Developer reference ·

Threads API Errors and Rate Limits

What every Threads API error means and how to fix it - error codes, media container failures, per-account quotas and app-level rate limits - plus an error decoder and tested retry code that backs off safely without ever posting twice.

250posts per account in a rolling 24 hours
1,000replies per account in a rolling 24 hours
4,800 ×impressions = your app's calls per 24 hours
24 hbefore an unpublished media container expires
Error anatomy

Anatomy of a Threads API error

The Threads API follows Graph API conventions: when a call fails, the response contains an error object. Five fields tell you almost everything you need.

Error response
{
  "error": {
    1"message": "Error validating access token: Session has expired",
    2"type": "OAuthException",
    3"code": 190,
    4"error_subcode": 463,
    5"fbtrace_id": "AbC123xYz789"
  }
}
  1. 1

    message

    A human-readable description. Useful for logs - but don't branch your code on it, as wording can change.

  2. 2

    type

    The error family, such as OAuthException for token and permission problems.

  3. 3

    code

    The number to act on - it tells you whether to fix, re-authorize or retry. See the code reference.

  4. 4

    error_subcode

    Extra detail when present - for example, why a token is invalid.

  5. 5

    fbtrace_id

    A unique ID for this request. Always log it - Meta support needs it to trace what happened.

Error decoder

Threads API error decoder

Paste an error response (or a container status response) to see what it means, whether to retry, and how to fix it. It runs in your browser - nothing is sent or stored. Remove any access tokens before pasting.

Try a sample:
Error codes

Threads API error codes reference

The codes developers most often see, what causes them, and what to do. Filter by category or search.

CodeMeaningCategoryRetry?How to fix
1Unknown errorA temporary or unexpected problem on Meta's side.ServerRetry with backoffRetry read calls with backoff. If it keeps happening, check Meta's platform status and log the fbtrace_id.
2Service temporarily unavailableTemporary downtime or overload.ServerRetry with backoffWait and retry read calls with backoff. For publish calls, check the container status before trying again.
4Application request limit reachedYour app has made too many calls in the current window.Rate limitRetry with backoffBack off exponentially, spread calls out, cache responses and replace polling with webhooks.
10Permission deniedThe app or user isn't allowed to do this.PermissionsFix firstRequest the right permission and, for production users, get advanced access through App Review.
17User request limit reachedToo many calls for one user account.Rate limitRetry with backoffSlow down calls for that account and retry with backoff.
100Invalid parameterA parameter is missing, misspelled or has an invalid value - for example text over 500 characters, an unsupported media_type, or a topic tag with a period.RequestFix firstCheck parameter names and values against the docs, then send a corrected request.
190Invalid or expired access tokenThe token expired, was revoked, or is malformed.AuthFix firstSend the user through OAuth again. Refresh long-lived tokens well before their 60 days are up.
200–299Permission errorA permission (scope) the endpoint needs wasn't granted.PermissionsFix firstAdd the scope to your authorization request and have the user re-authorize.
341Application limit reachedA temporary limit has been applied to your app.Rate limitRetry with backoffPause, then retry later with backoff. Reduce call volume.
368Temporarily blockedThe action was blocked, often because it looks spammy or raises policy concerns.PolicyFix firstStop the action, review your content and posting volume, and try again later.
613Rate limit exceededCalls exceeded a rate limit.Rate limitRetry with backoffBack off exponentially and lower your request frequency.

These are standard Graph API codes that the Threads API also uses. Exact messages and subcodes can vary, so always log the full error object and fbtrace_id.

Media errors

Media container statuses and errors

Images and videos are processed before they can be published. Check a container's status - Meta recommends about once a minute, for no more than 5 minutes. A container that isn't published within 24 hours expires.

IN_PROGRESS FINISHED PUBLISHED ERROR EXPIRED processingthreads_publish processing failednot published in 24h ✓ live
error_messageWhat went wrongFix
FAILED_DOWNLOADING_VIDEOThreads couldn't download the video from your URL.Use a public, direct file URL - no login pages, redirects or expiring links - on a fast host.
FAILED_PROCESSING_AUDIOThe audio track couldn't be processed.Re-encode the audio as AAC and upload again.
FAILED_PROCESSING_VIDEOThe video couldn't be processed.Re-encode as MP4 (H.264 video, AAC audio) and try again.
INVALID_ASPECT_RATIOThe video's shape is outside the allowed range.Keep between 0.01:1 and 10:1 - 9:16 vertical is recommended.
INVALID_BIT_RATEThe video's bitrate is outside the allowed range.Re-export with a standard bitrate preset.
INVALID_DURATIONThe video is too long.Keep videos to 5 minutes or less.
INVALID_FRAME_RATEThe frame rate is outside the allowed range.Use 23–60 frames per second.
INVALID_AUDIO_CHANNELSUnsupported number of audio channels.Use mono or stereo audio.
INVALID_AUDIO_CHANNEL_LAYOUTUnsupported audio channel layout.Re-export the audio as standard mono or stereo.
UNKNOWNAn unspecified processing error.Retry once with a fresh container. If it fails again, re-encode the file and check the URL.
Rate limits

Threads API rate limits explained

There are two kinds of limit: per-account quotas on actions like posting and replying, and an app-level call limit on how many requests your app makes overall.

Per-account quotas

Postsrolling 24 hours
250
Repliesrolling 24 hours
1,000
Deletesrolling 24 hours
100
Location searchesrolling 24 hours
500
Keyword searchesrolling 7 days
500

Bars show example usage. Quotas roll over continuously - there's no daily reset time.

Check remaining quota
curl -G "https://graph.threads.net/v1.0/$THREADS_USER_ID/threads_publishing_limit" \
  -d fields=quota_usage,config,reply_quota_usage,reply_config,delete_quota_usage,location_search_quota_usage \
  -d access_token=$THREADS_TOKEN

# → { "data": [ {
#       "quota_usage": 37,
#       "config": { "quota_total": 250, "quota_duration": 86400 },
#       "reply_quota_usage": 212,
#       "reply_config": { "quota_total": 1000, "quota_duration": 86400 },
#       ...
#     } ] }

App-level call limit

Your app can make 4,800 × impressions calls in a rolling 24 hours, where impressions is how many times content from the connected Threads account was seen on screen in that period. If impressions are under 10, Meta counts 10 - so every app gets at least 48,000 calls a day. There are matching limits on total CPU time and request time.

💡 Heavy polling is the usual way apps hit this. Cache responses, batch work, and use webhooks instead of asking "anything new?" every few seconds.

Calls per 24 h12,000,000
Average per minute8,333

Retries

How to retry Threads API errors safely

Retry only errors that can succeed on a second try, wait longer each time, and never resend a publish that might already have gone through.

Retry with backoff

  • Rate limits: codes 4, 17, 341, 613 - the request was rejected, so it's safe
  • Temporary errors: codes 1, 2 and 5xx responses - for read (GET) calls
  • Network drops and timeouts - for read (GET) calls
  • Containers still IN_PROGRESS - check again in about a minute

Fix first, don't retry

  • 190 - get a new token
  • 10 and 200–299 - request the missing permission
  • 100 - correct the parameter
  • 368 - stop and review what you're posting
  • Container ERROR - fix the media file or URL
cap 60s 1s2s4s8s16s32s retry 1retry 2retry 3retry 4retry 5retry 6

Wait time doubles each retry Random jitter (up to 1s) so many clients don't retry at the same moment

⚠️

Never blindly retry a publish

If a threads_publish call times out, the post may already be live. Retrying could post it twice. Instead, check the container's status: if it's PUBLISHED, you're done. The helpers below only retry POSTs when they were clearly throttled, and publish_container() checks first.

"""threads_retry.py - call the Threads API with safe retries and backoff."""
import random
import time

import requests

BASE_URL = "https://graph.threads.net/v1.0"
RATE_LIMIT_CODES = {4, 17, 341, 613}   # throttled: the request was rejected
TRANSIENT_CODES = {1, 2}               # temporary problems on Meta's side


class ThreadsError(Exception):
    """A Threads API error with its code, subcode and trace id."""

    def __init__(self, error, status=None):
        self.code = error.get("code")
        self.subcode = error.get("error_subcode")
        self.trace_id = error.get("fbtrace_id")
        self.status = status
        super().__init__(f"[{self.code}/{self.subcode}] {error.get('message')} "
                         f"(fbtrace_id={self.trace_id})")


def _backoff(attempt):
    """Exponential backoff with jitter: ~1s, 2s, 4s, 8s, 16s... capped at 60s."""
    time.sleep(min(60, 2 ** attempt) + random.uniform(0, 1))


def call(method, path, token, max_retries=5, **params):
    """Call the API. GETs retry on temporary errors; POSTs only retry when
    throttled, so a request that may have succeeded is never sent twice."""
    params["access_token"] = token
    for attempt in range(max_retries + 1):
        last = attempt == max_retries
        try:
            if method == "GET":
                resp = requests.get(f"{BASE_URL}/{path}", params=params, timeout=30)
            else:
                resp = requests.post(f"{BASE_URL}/{path}", data=params, timeout=30)
        except (requests.ConnectionError, requests.Timeout):
            if method != "GET" or last:
                raise
            _backoff(attempt)
            continue

        try:
            data = resp.json()
        except ValueError:
            data = {"error": {"message": f"HTTP {resp.status_code}"}}
        error = data.get("error")
        if not error:
            return data

        code = error.get("code")
        throttled = code in RATE_LIMIT_CODES
        transient = code in TRANSIENT_CODES or resp.status_code >= 500
        if last or not (throttled or (transient and method == "GET")):
            raise ThreadsError(error, resp.status_code)
        _backoff(attempt)


def publish_container(user_id, container_id, token):
    """Publish a container - skipping it if an earlier attempt already did."""
    info = call("GET", container_id, token, fields="status,error_message")
    if info.get("status") == "PUBLISHED":
        return None  # already live: don't post twice
    if info.get("status") != "FINISHED":
        raise RuntimeError(f"Container not ready: {info}")
    return call("POST", f"{user_id}/threads_publish", token,
                creation_id=container_id)["id"]
// threadsRetry.mjs - Node.js 18+
const BASE_URL = "https://graph.threads.net/v1.0";
const RATE_LIMIT_CODES = new Set([4, 17, 341, 613]); // throttled: request rejected
const TRANSIENT_CODES = new Set([1, 2]);              // temporary, Meta's side

export class ThreadsError extends Error {
  constructor(error, status) {
    super(`[${error.code}/${error.error_subcode}] ${error.message} (fbtrace_id=${error.fbtrace_id})`);
    this.code = error.code;
    this.subcode = error.error_subcode;
    this.traceId = error.fbtrace_id;
    this.status = status;
  }
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Exponential backoff with jitter: ~1s, 2s, 4s, 8s, 16s... capped at 60s
const backoff = (attempt) => sleep(Math.min(60, 2 ** attempt) * 1000 + Math.random() * 1000);

// GETs retry on temporary errors; POSTs only retry when throttled,
// so a request that may have succeeded is never sent twice.
export async function call(method, path, token, params = {}, maxRetries = 5) {
  const body = new URLSearchParams({ ...params, access_token: token });
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const last = attempt === maxRetries;
    let res;
    try {
      res = method === "GET"
        ? await fetch(`${BASE_URL}/${path}?${body}`, { signal: AbortSignal.timeout(30000) })
        : await fetch(`${BASE_URL}/${path}`, { method, body, signal: AbortSignal.timeout(30000) });
    } catch (err) {
      if (method !== "GET" || last) throw err;
      await backoff(attempt);
      continue;
    }
    const data = await res.json().catch(() => ({ error: { message: `HTTP ${res.status}` } }));
    if (!data.error) return data;
    const throttled = RATE_LIMIT_CODES.has(data.error.code);
    const transient = TRANSIENT_CODES.has(data.error.code) || res.status >= 500;
    if (last || !(throttled || (transient && method === "GET"))) {
      throw new ThreadsError(data.error, res.status);
    }
    await backoff(attempt);
  }
}
Debugging

Threads API debugging checklist

Work through these in order - most problems are found in the first three.

  1. Log the whole errorSave code, error_subcode, message, fbtrace_id and the endpoint you called - never just "request failed".
  2. Test the tokenCall GET /me?fields=id,username. A 190 here means the token, not your request, is the problem.
  3. Check permissionsConfirm the user granted the scope the endpoint needs, and that your app has advanced access for it in production.
  4. Check your quotaCall threads_publishing_limit to see if you've hit the posting, reply or delete limit.
  5. Check the containerFor media, request status,error_message on the container and fix any ERROR reason.
  6. Reproduce in Graph API ExplorerMake the same call by hand. If it works there, compare parameters with your code.
  7. Ask with the trace IDStill stuck? Post on the Meta developer community or file a bug with the fbtrace_id and a timestamp.
FAQ

Threads API errors FAQ

What does Threads API error 190 mean?
Error 190 means the access token is invalid or expired - for example, it passed its 60-day lifetime, the user removed your app, or the token was copied incorrectly. Retrying won't help: get a new token by sending the user through OAuth again, and refresh long-lived tokens before they expire.
What is the Threads API rate limit?
Per account: 250 posts, 1,000 replies, 100 deletes and 500 location searches in a rolling 24 hours, plus 500 keyword searches per rolling 7 days. Per app: 4,800 calls × impressions in 24 hours, with a minimum of 10 impressions (48,000 calls).
How do I check my remaining Threads API quota?
Call GET /{threads-user-id}/threads_publishing_limit with fields such as quota_usage, config, reply_quota_usage, reply_config, delete_quota_usage and location_search_quota_usage. Subtract usage from the configured total to get what's left.
What happens when I hit a Threads API rate limit?
Calls are rejected with a rate-limit error until usage drops back under the limit. Because limits use rolling windows, capacity frees up gradually. Back off exponentially, spread your calls out, and avoid frequent polling.
How often should I check a media container's status?
Meta recommends querying a container's status about once per minute, for no more than 5 minutes. Unpublished containers expire after 24 hours.
Is it safe to retry a failed Threads publish call?
Only if the error clearly shows the request was rejected, such as a rate-limit error. If the call timed out or failed with a server error, check the container's status first - if it's PUBLISHED, the post is already live and retrying would duplicate it.