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.
- 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.
| Method | When to Use | Time to Implement | Risk |
|---|---|---|---|
| Exponential backoff with jitter | HTTP 429 or intermittent 503 | 30 minutes | Low — standard production pattern, no API changes required |
| Bearer Token regeneration | HTTP 401 after credential rotation or accidental invalidation | 5 minutes | Low — old token is revoked immediately; update all consumers |
| OAuth 2.0 token refresh flow | HTTP 401 from expired user-context access token (2-hour TTL) | 1 hour | Low — requires storing and cycling refresh tokens |
| Upgrade API access tier | Persistent 403 on Pro/Enterprise-only endpoints like full-archive search | 1–5 business days | Medium — significant cost increase; requires plan review |
| Request deduplication and caching | Repeated identical requests consuming rate budget unnecessarily | 2–4 hours | Low — reduces API load without any API changes |
| Switch polling to filtered stream | High-volume tweet ingestion hitting GET endpoint rate caps | 1–2 days | Low — streaming uses separate rate buckets with persistent connections |
| Re-authorize with correct OAuth scopes | 403 on write or DM endpoints despite valid credentials | 1 hour | Low — 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/tweetswith an OAuth 2.0 token that only hastweet.readscope instead oftweet.write - Your developer application has been suspended for Terms of Service violations
- Attempting to access Direct Messages without the
dm.readordm.writescopes
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 windowx-rate-limit-remaining— how many requests you have left before hitting the capx-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
- Navigate to developer.twitter.com and open your Project and App
- Under App permissions, confirm the permission level (Read, Read+Write, or Read+Write+Direct Messages) matches what your code requires
- Under your Project, confirm your access level (Free, Basic, Pro, Enterprise)
- 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
#!/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.txtError 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
- https://developer.twitter.com/en/docs/twitter-api/rate-limits
- https://developer.twitter.com/en/support/twitter-api/error-troubleshooting
- https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api
- https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code
- https://stackoverflow.com/questions/tagged/twitter-api
- https://twittercommunity.com/c/twitter-api/62