Error Medic

Twitter API Rate Limit Errors: Fix 401, 403, 429 & 503 Responses

Fix Twitter API 429 rate limit, 401 unauthorized, 403 forbidden, and 503 errors. Step-by-step guide with real commands, retry logic, and root-cause diagnosis.

Last updated:
Last verified:
2,749 words
Key Takeaways
  • HTTP 429 ('Too Many Requests') means you have exceeded a Twitter API rate limit window — windows reset every 15 minutes; always read the x-rate-limit-reset header and sleep until that timestamp rather than guessing.
  • HTTP 401 ('Unauthorized') means your Bearer Token or OAuth credentials are absent, expired, or have been regenerated — go to developer.twitter.com and regenerate or refresh your token immediately.
  • HTTP 403 ('Forbidden') signals a permissions mismatch — your app's access tier (Free, Basic, Pro, Enterprise) does not include the endpoint you are calling, or your OAuth 2.0 token is missing required scopes like tweet.write or dm.read.
  • HTTP 503 ('Service Unavailable') is a Twitter infrastructure outage — check https://api.twitterstat.us before debugging your code; implement exponential backoff with jitter and never retry in a tight loop.
  • Quick fix summary: read x-rate-limit-remaining on every response, cache results aggressively with a 15-minute TTL, replace polling with filtered streams for high-volume ingestion, and ensure your OAuth scopes and API tier match the endpoints you are calling.
Fix Approaches Compared
MethodWhen to UseTime to ImplementRisk
Exponential backoff with jitterHTTP 429 or intermittent 50330 minutesLow — standard production pattern, no API changes required
Bearer Token regenerationHTTP 401 after credential rotation or accidental invalidation5 minutesLow — old token is revoked immediately; update all consumers
OAuth 2.0 token refresh flowHTTP 401 from expired user-context access token (2-hour TTL)1 hourLow — requires storing and cycling refresh tokens
Upgrade API access tierPersistent 403 on Pro/Enterprise-only endpoints like full-archive search1–5 business daysMedium — significant cost increase; requires plan review
Request deduplication and cachingRepeated identical requests consuming rate budget unnecessarily2–4 hoursLow — reduces API load without any API changes
Switch polling to filtered streamHigh-volume tweet ingestion hitting GET endpoint rate caps1–2 daysLow — streaming uses separate rate buckets with persistent connections
Re-authorize with correct OAuth scopes403 on write or DM endpoints despite valid credentials1 hourLow — existing users must re-authorize; old tokens become invalid

Understanding Twitter API Error Codes

The Twitter API v2 (also branded as the X API) surfaces four primary HTTP error codes that developers encounter in production: 401 Unauthorized, 403 Forbidden, 429 Too Many Requests, and 503 Service Unavailable. Each maps to a distinct root cause and requires a different fix. Conflating them wastes hours of debugging time.


Decoding Each Status Code

429 Too Many Requests — Rate Limited

This is the most common production error. Twitter enforces rate limits per endpoint, per 15-minute window, and per authentication context (app-level vs. user-level). When you exceed the cap, the response body looks like this:

HTTP/2 429
{"title":"Too Many Requests","detail":"Too Many Requests","status":429,"type":"about:blank"}

Rate limits vary dramatically by endpoint and tier. On the Free tier, POST /2/tweets allows 17 tweets per 24 hours per user. On Basic, GET /2/tweets/search/recent allows 10 requests per 15 minutes at the app level. Pro and Enterprise tiers unlock substantially higher caps. Always check the official rate limits page for the specific endpoint you are calling.

401 Unauthorized — Invalid or Missing Credentials

A 401 appears when:

  • The Authorization: Bearer <token> header is absent or malformed
  • The Bearer Token was regenerated in the Developer Portal (invalidating the previous one)
  • An OAuth 1.0a signature is incorrect due to timestamp skew or a wrong consumer secret
  • An OAuth 2.0 user-context access token has expired and has not been refreshed

Exact response body:

HTTP/2 401
{"title":"Unauthorized","type":"https://api.twitter.com/2/problems/unauthorized","status":401,"detail":"Unauthorized"}

403 Forbidden — Permission or Tier Mismatch

A 403 means your credentials are valid, but you are not permitted to perform the requested action. Common scenarios include:

  • Calling GET /2/tweets/search/all (full-archive search) on a Free or Basic plan — this requires Pro or Enterprise
  • Attempting POST /2/tweets with an OAuth 2.0 token that only has tweet.read scope instead of tweet.write
  • Your developer application has been suspended for Terms of Service violations
  • Attempting to access Direct Messages without the dm.read or dm.write scopes

Exact response body:

HTTP/2 403
{"title":"Forbidden","type":"https://api.twitter.com/2/problems/not-authorized-for-resource","status":403,"detail":"You are not permitted to perform this action."}

503 Service Unavailable — Twitter Infrastructure Issues

This is a server-side failure at Twitter. It is transient in the vast majority of cases and resolves without any action on your part. Do not retry immediately — hammering a 503 amplifies load and can trigger additional rate limiting.

HTTP/2 503
{"errors":[{"message":"Twitter is over capacity.","code":130}]}

Step 1: Diagnose the Error Source

Always Read the Response Headers First

Before modifying any code, inspect the rate-limit headers on every response:

x-rate-limit-limit: 900
x-rate-limit-remaining: 0
x-rate-limit-reset: 1708704600
  • x-rate-limit-limit — the total request cap for this endpoint in the current window
  • x-rate-limit-remaining — how many requests you have left before hitting the cap
  • x-rate-limit-reset — Unix timestamp when the window resets and your cap refills

If x-rate-limit-remaining is 0 and you received a 429, you are genuinely rate limited. Sleep until x-rate-limit-reset and do not retry before that time.

Validate Your Credentials with a Minimal Request

A quick sanity check before debugging further:

curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $TWITTER_BEARER_TOKEN" \
  "https://api.twitter.com/2/users/me"
  • Returns 200 — token is valid; the problem is elsewhere
  • Returns 401 — token is invalid, missing, or malformed; regenerate it
  • Returns 403 — token is valid but your app lacks the required permissions

Check Your App's Access Level and Permissions

  1. Navigate to developer.twitter.com and open your Project and App
  2. Under App permissions, confirm the permission level (Read, Read+Write, or Read+Write+Direct Messages) matches what your code requires
  3. Under your Project, confirm your access level (Free, Basic, Pro, Enterprise)
  4. Cross-reference the endpoint you are calling against the rate limits documentation to verify it is available at your tier

Check Twitter's Status Page for 503

For 503 errors, always check https://api.twitterstat.us before writing a single line of debug code. If there is an active incident, your debugging effort is wasted. Wait for the incident to resolve.


Step 2: Fix Rate Limiting (429)

Implement Exponential Backoff with Jitter

The correct retry pattern honors x-rate-limit-reset for 429 errors and uses exponential backoff for 503 errors:

import time
import random
import requests

def request_with_backoff(url, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)

        if response.status_code == 200:
            return response

        if response.status_code == 429:
            reset = int(response.headers.get('x-rate-limit-reset', 0))
            wait = max(reset - time.time(), 1) + random.uniform(0, 2)
            print(f'Rate limited. Sleeping {wait:.1f}s until window reset.')
            time.sleep(wait)
            continue

        if response.status_code == 503:
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f'503 error. Backoff {wait:.1f}s (attempt {attempt + 1}/{max_retries})')
            time.sleep(wait)
            continue

        if response.status_code in (401, 403):
            raise PermissionError(f'Auth error {response.status_code}: {response.text}')

        response.raise_for_status()

    raise RuntimeError(f'Max retries exceeded for {url}')

Cache Results to Reduce Request Volume

Many 429 errors arise from repeated identical calls. Cache responses with a TTL that matches the rate limit window:

import time

_tweet_cache = {}
CACHE_TTL_SECONDS = 900  # 15 minutes — aligns with Twitter rate limit windows

def get_tweet_cached(tweet_id, headers):
    now = time.time()
    entry = _tweet_cache.get(tweet_id)
    if entry and now - entry['ts'] < CACHE_TTL_SECONDS:
        return entry['data']
    response = request_with_backoff(
        f'https://api.twitter.com/2/tweets/{tweet_id}', headers
    )
    _tweet_cache[tweet_id] = {'data': response.json(), 'ts': now}
    return _tweet_cache[tweet_id]['data']

Upgrade Your Access Tier When Rate Limits Are Structurally Insufficient

If backoff and caching are properly implemented but you still consistently exhaust limits, the cap is simply too low for your use case. The tier breakdown:

Tier Monthly Cost Recent Search Limit Full Archive Search Write Cap/Month
Free $0 1 req/15 min Not available 500 tweets
Basic $100 10 req/15 min Not available 3,000 tweets
Pro $5,000 300 req/15 min 300 req/15 min 300,000 tweets
Enterprise Custom Custom Custom Custom

Step 3: Fix Authentication Errors (401)

Regenerate Your Bearer Token

If you suspect your token has been compromised or invalidated, regenerate it immediately: go to developer.twitter.com, open your App, navigate to Keys and Tokens, and click Regenerate next to Bearer Token. The old token is invalidated the instant you regenerate. Update every service and secret manager that holds the old value before restarting.

Handle OAuth 2.0 Access Token Expiry

User-context OAuth 2.0 access tokens expire after approximately 2 hours by default. Implement automatic refresh:

def refresh_oauth2_token(refresh_token, client_id, client_secret):
    response = requests.post(
        'https://api.twitter.com/2/oauth2/token',
        data={
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'client_id': client_id,
        },
        auth=(client_id, client_secret)
    )
    response.raise_for_status()
    tokens = response.json()
    # Persist both new access_token and new refresh_token
    return tokens['access_token'], tokens['refresh_token']

Store the new refresh token after each refresh — Twitter issues a new refresh token on every call and the previous one is invalidated.


Step 4: Fix Permission Errors (403)

Verify OAuth 2.0 Scopes Match the Operation

OAuth 2.0 tokens are scoped at creation time. Required scopes by operation type:

  • Read tweets and users: tweet.read users.read
  • Post tweets: tweet.write users.read
  • Read Direct Messages: dm.read users.read
  • Send Direct Messages: dm.write users.read
  • Read and write lists: list.read list.write users.read

If an existing token was created with insufficient scopes, you must revoke it and trigger a new authorization flow with the correct scope list. Changing app-level permissions in the Developer Portal also invalidates all existing user tokens.

Verify the Endpoint Is Available at Your Tier

The most common 403 trap is calling a Pro or Enterprise-only endpoint from a Free or Basic app. Always check the endpoint's tier requirements in the API reference before writing integration code. The detail field in the 403 response body often specifies whether the issue is tier-related or scope-related, which narrows the diagnosis immediately.

Frequently Asked Questions

bash
#!/usr/bin/env bash
# Twitter API Diagnostic Script
# Usage: TWITTER_BEARER_TOKEN=your_token bash diagnose_twitter_api.sh

set -euo pipefail

BASE_URL='https://api.twitter.com/2'
TOKEN="${TWITTER_BEARER_TOKEN:-}"

if [ -z "$TOKEN" ]; then
  echo 'ERROR: Set TWITTER_BEARER_TOKEN environment variable.'
  exit 1
fi

echo '=== Twitter API Diagnostics ==='
echo "Timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo ''

# 1. Check Twitter infrastructure status
echo '[1] Checking Twitter API status page...'
STATUS_JSON=$(curl -sf 'https://api.twitterstat.us/api/v2/status.json' 2>/dev/null || echo '{}')
STATUS_DESC=$(echo "$STATUS_JSON" | python3 -c \
  'import json,sys; d=json.load(sys.stdin); print(d.get("status",{}).get("description","Unknown"))' \
  2>/dev/null || echo 'Could not reach status page')
echo "    Status: $STATUS_DESC"
echo ''

# 2. Validate Bearer Token
echo '[2] Validating Bearer Token...'
HTTP_CODE=$(curl -s -o /tmp/tw_auth.json -w '%{http_code}' \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE_URL/users/me")

echo "    HTTP Status: $HTTP_CODE"
if [ "$HTTP_CODE" = '200' ]; then
  USERNAME=$(python3 -c \
    'import json; d=json.load(open("/tmp/tw_auth.json")); print(d["data"]["username"])' \
    2>/dev/null || echo 'unknown')
  echo "    Authenticated as: @$USERNAME"
elif [ "$HTTP_CODE" = '401' ]; then
  echo '    RESULT: 401 Unauthorized -- Token invalid or missing.'
  echo '    ACTION: Regenerate at developer.twitter.com -> App -> Keys and Tokens'
elif [ "$HTTP_CODE" = '403' ]; then
  echo '    RESULT: 403 Forbidden -- Token valid but permissions insufficient.'
  echo '    DETAIL:'
  python3 -c 'import json; d=json.load(open("/tmp/tw_auth.json")); print("   ", d.get("detail","No detail"))' 2>/dev/null || true
fi
echo ''

# 3. Inspect rate limit headers for recent search endpoint
echo '[3] Inspecting rate limit headers (recent search endpoint)...'
curl -sf -o /dev/null \
  -D /tmp/tw_headers.txt \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE_URL/tweets/search/recent?query=from%3Atwitterdev&max_results=10" 2>/dev/null || true

if [ -f /tmp/tw_headers.txt ]; then
  RL_LIMIT=$(grep -i '^x-rate-limit-limit:' /tmp/tw_headers.txt 2>/dev/null | tr -d '\r' | awk '{print $2}' || echo 'N/A')
  RL_REMAINING=$(grep -i '^x-rate-limit-remaining:' /tmp/tw_headers.txt 2>/dev/null | tr -d '\r' | awk '{print $2}' || echo 'N/A')
  RL_RESET=$(grep -i '^x-rate-limit-reset:' /tmp/tw_headers.txt 2>/dev/null | tr -d '\r' | awk '{print $2}' || echo '')

  echo "    x-rate-limit-limit:     ${RL_LIMIT}"
  echo "    x-rate-limit-remaining: ${RL_REMAINING}"

  if [ -n "$RL_RESET" ]; then
    NOW=$(date +%s)
    WAIT=$(( RL_RESET - NOW ))
    RESET_HUMAN=$(date -d "@$RL_RESET" '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null \
      || date -r "$RL_RESET" '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null \
      || echo "Unix: $RL_RESET")
    echo "    x-rate-limit-reset:     $RESET_HUMAN"
    echo "    Seconds until reset:    ${WAIT}s"
    if [ "${RL_REMAINING:-1}" = '0' ]; then
      echo "    WARNING: Rate limit exhausted. Sleep ${WAIT}s before retrying."
    fi
  fi
else
  echo '    Could not retrieve headers (check token validity above)'
fi
echo ''

# 4. Probe endpoint tier availability
echo '[4] Probing access tier (full-archive search = Pro+ only)...'
ARCHIVE_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE_URL/tweets/search/all?query=from%3Atwitterdev&max_results=10" 2>/dev/null || echo '000')

case "$ARCHIVE_CODE" in
  200) echo '    Full-archive search: AVAILABLE  (Pro or Enterprise tier confirmed)' ;;
  403) echo '    Full-archive search: UNAVAILABLE (Free or Basic tier -- upgrade required)' ;;
  429) echo '    Full-archive search: RATE LIMITED (endpoint exists at your tier)' ;;
  *)   echo "    Full-archive search: HTTP $ARCHIVE_CODE" ;;
esac

echo ''
echo '=== Diagnostics complete ==='
rm -f /tmp/tw_auth.json /tmp/tw_headers.txt
E

Error Medic Editorial

The Error Medic Editorial team consists of senior DevOps engineers, SREs, and API integration specialists with experience across high-scale production systems at cloud-native companies. We specialize in translating cryptic HTTP error codes and rate-limiting behaviors into actionable, step-by-step resolutions backed by official documentation and real-world production experience.

Sources

Related Articles in Twitter Api

Explore More API Errors Guides