X Ads API Rate Limits: The Two-System Throttle That Breaks Campaign Syncs
A working X Ads API connection (see our developer account approval guide) does not mean your integration can pull data as fast as it wants. The Advertiser API runs two separate rate-limit systems at once — user token and ad account — on OAuth 1.0a, with per-minute and per-15-minute windows that have nothing to do with the core X API v2 limits most teams already know.
Two rate limits, not one
Every authenticated call carries a user-token-level limit. A subset of GET endpoints also carries a separate ad-account-level limit, returned in its own response headers. When both are present, X's own documentation says the ad-account-level limit should take precedence — the user-level number is set high on purpose, as a global ceiling for the whole application, not the number you should actually budget against.
User token headers
x-rate-limit-limit, x-rate-limit-remaining, x-rate-limit-reset — present on every call, but only the real ceiling when no ad-account header is returned.
Ad account headers
x-account-rate-limit-limit, x-account-rate-limit-remaining, x-account-rate-limit-reset — ad-account-scoped, GET endpoints only. Write actions are not confirmed to use the same account-level limits.
A sync job that reads only the user-level headers and ignores the account-level ones will look fine in testing on one ad account, then throttle unpredictably the moment a single OAuth token is used to manage several accounts at once — a common shape for agency and SaaS dashboard integrations.
The published rate limit chart
Unlike the core X API, the Ads API does not expose a single programmatic index of every endpoint's limit — but X does publish a category-level chart. These are the official numbers, not estimates:
| Endpoint type | Scope | Limit |
|---|---|---|
| Writes (POST, PUT, DELETE) | Category | 450 / 1-minute window |
| Audience | Endpoint | 1,500 / 1-minute window |
| Analytics (synchronous) | Category | 250 / 15-minute window |
| Core entity reads (campaigns, line items) | Endpoint, ad-account level | 10,000 / 15-minute window |
| Other account reads | Endpoint, partial ad-account level | 2,000 / 15-minute window |
| Targeting criteria (general) | Category | 400 / 15-minute window |
| Targeting criteria (tv_markets, tv_shows) | Endpoint | 2,000 / 15-minute window |
| Audience insights | Category | 400 / 15-minute window |
| Keyword insights | Category | 500 / 15-minute window |
| Global reads (no account_id in path) | Endpoint | 5 / 15-minute window |
| Conversions | Endpoint | 60,000 / 15-minute window |
The gotcha teams miss
Global read endpoints — the ones with no :account_id in the path — are capped at just 5 requests per 15 minutes. That is not a typo. Reference-data calls that look harmless in a quick test (feature lists, targeting taxonomies) can exhaust this ceiling almost immediately if they're called inside a loop instead of cached.
Where teams actually get stuck
Treating the two header sets as one
Code that parses only x-rate-limit-* silently ignores the more restrictive (or more generous) account-level ceiling sitting in x-account-rate-limit-* on the same response.
Assuming a limit increase is available
X's own FAQ states limits are generally not raised and are set to support the largest advertisers already on the platform — the documented first step is implementing the published best practices, not filing a request.
Confusing a 503 for a 429
OVER_CAPACITY and SERVICE_UNAVAILABLE are both HTTP 503 — a server-side condition, not a client-side throttle — but naive retry logic often treats them identically to a rate-limit backoff.
Missing the second 429 error code
The generic throttle is TOO_MANY_REQUESTS, but tweet-specific write actions can separately return TWEET_RATE_LIMIT_EXCEEDED — same HTTP status, different error code, different remediation path.
Skipping incremental sync
Re-pulling full campaign or line-item history on every run instead of filtering by last-synced timestamp burns through the 10,000-per-15-minute core-read ceiling far faster than necessary.
The error codes behind a failed sync
X Ads API errors return a CAPS_CASE errors/code field alongside an HTTP status. The codes that show up most often once volume grows:
| HTTP | Error code | What it actually means |
|---|---|---|
| 429 | TOO_MANY_REQUESTS | Standard rate-limit throttle — check both header sets before retrying |
| 429 | TWEET_RATE_LIMIT_EXCEEDED | Separate tweet-specific write throttle, not the general endpoint limit |
| 503 | OVER_CAPACITY | Server-side capacity issue, not a client quota problem |
| 503 | SERVICE_UNAVAILABLE | Transient server error — safe to retry with backoff, unrelated to rate limits |
| 400 | INVALID_PARAMETER | Malformed request value, most common cause of failed writes |
| 403 | ACCOUNT_LOCKED_OUT | Ad account access itself is blocked — no amount of retrying fixes this |
Why this doesn't map onto the core X API
Our X API 429 errors guide covers the core, pay-per-use X API v2 — OAuth 2.0, a monthly usage cap layered on top of a 15-minute rate window, and a single TOO_MANY_REQUESTS-style response. The Ads API shares almost none of that: it authenticates with OAuth 1.0a, uses 1-minute and 15-minute windows depending on the category, and layers a second, ad-account-scoped limit system on top of the standard one. Code written against the core API's throttling model does not transfer to the Ads API without changes.
What's confirmed vs. what isn't
Confirmed directly from X's own documentation: every limit in the chart above, the dual header system, the FAQ statement on not raising limits, and the full error-code table. Not published anywhere by X: a numeric ceiling behind TWEET_RATE_LIMIT_EXCEEDED specifically, or a fixed SLA for how "your X Ads API contacts" respond to a scaling request. Those are left out here rather than guessed.