Twitter/X API Rate Limits in 2026 — Complete Guide
The #1 Pain Point for Twitter API Developers
If you've worked with the X (formerly Twitter) API for any length of time, you've hit a rate limit. It's practically a rite of passage. The dreaded 429 Too Many Requests response has derailed more projects than any other single issue in the Twitter developer ecosystem.
In 2026, X's API rate limits remain one of the most confusing and frustrating aspects of building on the platform. The tier system has evolved since the Elon Musk acquisition, and understanding exactly what you get at each level — and how to work within those constraints — is critical for any serious project.
This guide covers everything: the current tier structure, how rate limit headers work, strategies for staying under the limit, and what to do when you inevitably hit one.
X API v2 Tier Overview (2026)
X currently offers three API access tiers, each with dramatically different limits:
Free Tier
max_results=100) more than once every few days.Basic Tier
Pro Tier
Quick Comparison Table
App-Level vs User-Level Rate Limits
One of the most confusing aspects of X's rate limiting is the distinction between app-level and user-level limits.
App-Level Limits
These apply to your entire application regardless of how many users are authenticated. If your app makes 50 requests in a 15-minute window on the Free tier, all subsequent requests will be rejected — even for different users.
App-level limits use App-Only authentication (Bearer Token). Most read endpoints fall under this category.
User-Level Limits
These apply per authenticated user. If you're building an app where users sign in with their X account (OAuth 2.0), each user gets their own rate limit bucket. This can effectively multiply your capacity.
For example, the GET /2/tweets/:id endpoint allows:
The catch? User-level auth requires OAuth 2.0 PKCE flow, which adds significant complexity to your application.
Understanding Rate Limit Headers
Every response from the X API includes three rate limit headers:
x-rate-limit-limit: 300
x-rate-limit-remaining: 247
x-rate-limit-reset: 1704067200x-rate-limit-limit: Maximum number of requests allowed in the current windowx-rate-limit-remaining: How many requests you have leftx-rate-limit-reset: Unix timestamp (seconds) when the window resetsAlways read these headers. They're your early warning system. Don't wait until you get a 429 — proactively check remaining and slow down when it gets low.
import requests import time def check_rate_limit(response): remaining = int(response.headers.get("x-rate-limit-remaining", 0)) reset_time = int(response.headers.get("x-rate-limit-reset", 0)) if remaining < 5: sleep_time = reset_time - int(time.time()) + 1 if sleep_time > 0: print("Rate limit nearly exhausted. Sleeping " + str(sleep_time) + "s...") time.sleep(sleep_time) return remaining
Handling 429 Too Many Requests
When you exceed your rate limit, the API returns a 429 status code with a body like:
{ "title": "Too Many Requests", "detail": "Too Many Requests", "type": "about:blank", "status": 429 }
The wrong way to handle this: immediately retry the request. This can cascade into a retry storm that keeps you locked out longer.
The right way: implement exponential backoff with jitter.
Exponential Backoff Implementation
import requests import time import random def request_with_backoff(url, headers, max_retries=5): """Make an API request with exponential backoff on 429 errors.""" for attempt in range(max_retries): response = requests.get(url, headers=headers) if response.status_code == 200: return response if response.status_code == 429: # Check the reset header first reset_time = int(response.headers.get("x-rate-limit-reset", 0)) now = int(time.time()) if reset_time > now: # Sleep until the window resets wait = reset_time - now + 1 print("Rate limited. Waiting " + str(wait) + "s until reset...") time.sleep(wait) else: # Fallback: exponential backoff with jitter base_wait = min(2 ** attempt, 64) jitter = random.uniform(0, base_wait * 0.5) wait = base_wait + jitter print("Rate limited (attempt " + str(attempt + 1) + "). Backing off " + str(round(wait, 1)) + "s...") time.sleep(wait) else: # Non-rate-limit error, raise immediately response.raise_for_status() raise Exception("Max retries exceeded after " + str(max_retries) + " attempts")
Why jitter matters: If 10 clients all hit the rate limit at the same time and all retry after exactly 2 seconds, they'll all collide again. Adding random jitter (noise) to the backoff spreads out the retries and reduces thundering herd problems.
Strategies to Maximize Your Quota
Rate limits don't have to ruin your project. Here are battle-tested strategies to get the most out of your allocation:
1. Aggressive Caching
Cache every response. Most Twitter data doesn't change second-by-second. A user's profile can be cached for 15-60 minutes. Tweet data can be cached even longer since tweets are immutable (aside from like/retweet counts).
import time class SimpleCache: def __init__(self): self.store = {} def get(self, key, ttl=900): """Get a cached value if it exists and hasn't expired.""" if key in self.store: value, timestamp = self.store[key] if time.time() - timestamp < ttl: return value return None def set(self, key, value): self.store[key] = (value, time.time()) cache = SimpleCache() def get_user(username, headers): cached = cache.get("user:" + username, ttl=900) # 15 min cache if cached: return cached url = "https://api.x.com/2/users/by/username/" + username response = request_with_backoff(url, headers) data = response.json() cache.set("user:" + username, data) return data
2. Batch Requests
Instead of fetching users one by one, use batch endpoints. The /2/users endpoint accepts up to 100 user IDs in a single request. That's 100x more efficient than individual lookups.
# Bad: 100 requests for 100 users for user_id in user_ids: response = requests.get( "https://api.x.com/2/users/" + user_id, headers=headers ) # Good: 1 request for 100 users ids_param = ",".join(user_ids[:100]) response = requests.get( "https://api.x.com/2/users?ids=" + ids_param, headers=headers )
3. Request Queuing
Instead of making requests as fast as possible, queue them and process at a controlled rate. This prevents bursts that eat through your 15-minute window.
import time from collections import deque class RateLimitedQueue: def __init__(self, max_per_window=150, window_seconds=900): self.max_per_window = max_per_window self.window_seconds = window_seconds self.timestamps = deque() def wait_if_needed(self): """Block until we can safely make another request.""" now = time.time() # Remove timestamps outside the current window while self.timestamps and self.timestamps[0] < now - self.window_seconds: self.timestamps.popleft() if len(self.timestamps) >= self.max_per_window: # Wait until the oldest request falls outside the window sleep_time = self.timestamps[0] - (now - self.window_seconds) + 0.1 if sleep_time > 0: print("Queue full. Sleeping " + str(round(sleep_time, 1)) + "s...") time.sleep(sleep_time) self.timestamps.append(time.time()) queue = RateLimitedQueue(max_per_window=140) # Leave 10 request buffer def queued_request(url, headers): queue.wait_if_needed() return requests.get(url, headers=headers)
4. Use Fields and Expansions Wisely
Every X API request supports tweet.fields, user.fields, and expansions parameters. Request only the fields you need. Smaller responses are faster and count the same toward your rate limit as large ones — so there's no penalty for being specific, but you reduce bandwidth and processing time.
# Instead of getting everything: url = "https://api.x.com/2/tweets/" + tweet_id # Only request what you need: url = ("https://api.x.com/2/tweets/" + tweet_id + "?tweet.fields=created_at,public_metrics" + "&expansions=author_id" + "&user.fields=username,verified")
5. Use Conditional Requests
For endpoints that support it, use If-None-Match headers with ETags. If the data hasn't changed, the API returns 304 Not Modified — which does not count against your rate limit.
Monthly Quotas: The Hidden Rate Limit
Beyond the per-window limits, X enforces monthly caps on tweet reads and writes. This is separate from the 15-minute window system and arguably more painful.
On the Basic tier, 10,000 tweet reads per month means:
max_results=50If you're building a monitoring dashboard that checks 20 accounts every hour, you'll blow through your monthly limit in about 3 days. This is where the math stops working for most Basic tier users.
There's no way to buy more monthly quota without upgrading tiers. You can't add $50 for another 5,000 reads. It's either $200/month for 10K or $5,000/month for 1M. The gap is brutal.
Common Pitfalls
Pagination Drains
Each page of results counts as a separate request. Fetching a user's followers with 50 per page across 20 pages uses 20 requests — and if that user has 100K followers, you'll never finish on Free or Basic.
Search is Expensive
The /2/tweets/search/recent endpoint has tighter limits than most other endpoints: 60 requests per 15 minutes on Basic. Complex queries with multiple operators don't reduce the count — each request is one request regardless of query complexity.
Streaming Costs
The filtered stream endpoint (/2/tweets/search/stream) is only available on Pro. There's no Basic tier access. If you need real-time data, you're looking at $5,000/month minimum.
Deleted Tweets Still Count
If you request a tweet that has been deleted, the API returns an error response — but it still counts against your rate limit. There's no way to avoid this.
Rate Limiting Best Practices Checklist
The Bigger Picture
Rate limits exist for a reason — they protect the platform from abuse and ensure fair access. But the current pricing structure creates a dead zone between Basic ($200/mo, 10K tweets) and Pro ($5,000/mo, 1M tweets) that's painful for mid-size projects.
Many developers spend more engineering time working around rate limits than building their actual product. Between implementing caching layers, backoff logic, request queues, and quota monitoring — the infrastructure overhead is significant.
Alternatively, services like XCROP provide higher limits without the complexity of managing Twitter's rate limit tiers. Instead of dealing with 15-minute windows and monthly caps, you get a simple credit-based system where each API call costs a fixed number of credits — no surprise throttling, no tier math.
Whatever approach you choose, understanding rate limits deeply is essential. The strategies in this guide will help you build more resilient applications and avoid the most common traps that catch developers off guard.