API GuidesAugust 11, 202610 min readingMarcel Czuryszkiewicz

X API Rate Limits and Caps

Three independent counters on X return the same 429: the 15-minute window, the per-endpoint daily cap, and your billing cap. This separates them by the error type field, shows which headers exist and which do not, and points out three different limits X's own documentation gives for POST /2/tweets.

X API rate limits are not one number. Three independent counters can each return the same HTTP 429: a per-endpoint window that resets in minutes, a per-endpoint 24-hour cap, and the billing cap on your prepaid usage. The type field in the error body is what tells them apart. Read the wrong one and your backoff sleeps five minutes for a limit that resets tomorrow, or for one that never resets on its own at all.

A black Time Timer countdown clock standing on a wooden desk, its red disc filling the segment between 0 and 15 minutes on the white dial, the room behind it out of focus
Fifteen minutes is a window, not a countdown you can shorten.

Three counters, one status code

All numbers below come from our snapshot of docs.x.com taken 7 May 2026.

1. The per-endpoint window. Usually 15 minutes, counted per app on a Bearer token and per user on OAuth 1.0a or an OAuth 2.0 user token. This is the only counter the response headers describe.

2. The per-endpoint daily cap. X's own table gives POST /2/tweets a per-app limit of 10,000 per 24 hours next to a per-user limit of 100 per 15 minutes. Same endpoint, same table, two clocks with nothing to do with each other.

3. The billing cap. On pay-per-use your credits run out, and self-serve plans carry a monthly ceiling of 2 million post reads. Time does not fix this one.

X's error documentation separates the first two from the third by type:

{
  "title": "Too Many Requests",
  "detail": "Too Many Requests",
  "type": "https://api.x.com/2/problems/usage-capped"
}

.../rate-limit-exceeded means you were too fast. .../usage-capped means you spent the budget. X's own status table calls 429 "Rate limit or usage cap exceeded": one status, two meanings, stated on the page. Our error adapter pulls the slug out of /problems/<slug> and builds TW:rate-limit-exceeded or TW:usage-capped, because the two need opposite handling.

X still returns the older shape on some 429s, {"errors":[{"code":88,"message":"Rate limit exceeded"}]}, which is the example the rate limits page itself uses. Parse both.

Which header tells you which

Three headers, documented identically on the rate limits page and the response codes page:

HeaderWhat it measuresUnitWhat to do with it
x-rate-limit-limitCeiling for the current window on this endpointrequestsBudget against it, do not hardcode it
x-rate-limit-remainingRequests left in the current windowrequestsTreat 0 as already limited
x-rate-limit-resetWhen the current window resetsUnix timestamp, secondsSleep until it, then retry once

That is the complete documented set. No header reports your 24-hour cap, and none reports your credit balance. We grepped our full snapshot of docs.x.com for a 24-hour header family and got zero hits.

The consequence is the bug most people ship. A 429 raised by the daily cap still carries an x-rate-limit-reset pointing at the next 15-minute boundary, because that is the only window the header knows about. Code that backs off to -reset and retries wakes up in a few minutes and hits the wall again, until the retry budget is gone. The daily cap is yours to count: a rolling 24-hour counter per endpoint, split by app and by user, that stops before X does.

One free signal: x-rate-limit-remaining: 0 can arrive on a successful 200. Our adapter treats a remaining of 0 as rate-limited regardless of status, which turns the last call of a window into a warning instead of a surprise.

Two identical coin-operated parking meters side by side outdoors, both capped with snow, Park and Pay stickers on their fronts and a snow-covered park blurred behind them
Two meters, two clocks, and neither one knows about the other.

The limits that matter if you publish

The full tables run to roughly ninety rows. These are the ones a publishing integration touches, as of the 7 May 2026 snapshot:

MethodEndpointPer appPer user
POST/2/tweets10,000/24hrs100/15min (see conflict below)
DELETE/2/tweets/:idn/a50/15min
POST/2/users/:id/retweetsn/a50/15min
POST/2/media/upload50,000/24hrs500/15min
POST/2/media/upload/initialize180,000/24hrs1,875/15min
POST/2/media/upload/:id/append180,000/24hrs1,875/15min
POST/2/media/upload/:id/finalize180,000/24hrs1,875/15min
GET/2/usage/tweets50/15minn/a

Two things to notice. The media rows describe /2/media/* endpoints, which is not the upload path most integrations actually use; see "Media upload runs on a different API" below. And the endpoint that reports your usage is itself capped at 50 calls per 15 minutes, so polling it as a live gauge is not a plan.

For the same tables across Meta, TikTok, LinkedIn and the rest, we keep a comparison in social media API rate limits.

Where X's own docs disagree

Three different limits for POST /2/tweets, all present in the same documentation set on the same day:

  • Rate limits page: 100 requests per 15 minutes per user, 10,000 per 24 hours per app.
  • Manage Posts integration page: "a user rate limit of 200 requests per 15 minutes for the POST method."
  • Same page, and the migration page: "a limit of 300 requests per 3 hours, including Posts created with either manage Posts or manage Retweets."

100 against 200 for the same endpoint in the same auth context. And the third number introduces a window that appears nowhere else: three hours, shared with reposts.

Plan against the lowest effective rate, which is not the smallest-looking number. 100 per 15 minutes is 400 per hour. 300 per 3 hours is 100 per hour. The 3-hour rule is the binding constraint and the one to size a per-account queue against. A scheduler pacing one account faster than roughly a post every 36 seconds finds that limit whichever 15-minute number is true.

Media upload runs on a different API

The rate limits page lists eight /2/media/* rows. The manage Posts page, in the same documentation set, says: "Currently, isn't a way to fully upload media using v2 of the X API currently." Quoted verbatim, typos included, because it contradicts the table two pages away.

What we run in production is the v1.1 path: INIT, APPEND, FINALIZE against https://upload.x.com/1.1/media/upload.json, signed with OAuth 1.0a, in 5 MB chunks. Whether the published /2/media/* numbers govern that host is not something the documentation states.

For rate limiting the shape matters more than the number, because one video is not one request:

const CHUNK_SIZE = 5_000_000; // 5 MB, what we send in production
const uploadCalls = 1 + Math.ceil(fileBytes / CHUNK_SIZE) + 1; // INIT + APPENDs + FINALIZE

// 200 MB video → 1 + 40 + 1 = 42 calls, before a single STATUS poll
// and before the POST /2/tweets that attaches it

A queue that budgets "one post, one request" is wrong by a factor of forty on video. The full upload sequence, including the processing poll, is in posting a tweet with the X API; the four calls themselves, chunk sizing and the auth split between them are in X API media upload.

bundle.social

Publish to X without watching three counters.

$0.015 per post, $0.200 for a post containing a link. No quote call, no minimum.

Four loading bays along the side of a pale warehouse building, roller shutter doors closed behind black rubber dock seals, the concrete apron in front of them empty
A queue that stops moving looks the same whether it is throttled or out of credit.

Rejections that look like rate limits

These arrive as failed publishes and get reported as rate limits. None of them are.

SymptomActual causeFix
"not allowed to create a Post with duplicate content"X refuses identical text from the same account. X documents neither the window nor the similarity thresholdChange the text. Never retry
Reply rejected while the account is far under every limitSelf-serve rule: a reply only lands if the original author @mentioned or quoted the replying account. Enterprise is exemptNot fixable by waiting
Post with two $TICKER symbols rejectedSelf-serve cap of one cashtag per postStrip to one
A 429 that never clearsusage-capped: credits exhausted, or the monthly 2 million post read ceilingTop up, alert a human

One correction worth stating outright, because competing articles get it wrong. X's daily deduplication is a billing rule: the same post retrieved twice within a day counts once toward usage. It has nothing to do with the duplicate-content rejection you get when publishing. Same phrase, different subsystem. The billing side is covered in X API pricing and costs.

How to retry each one

429 is a retryable status. Whether retrying is useful depends on which counter tripped.

if (status === 429) {
  const type: string = body?.type ?? ""; // https://api.x.com/2/problems/<slug>

  // Budget exhausted. Waiting changes nothing.
  if (type.endsWith("/usage-capped")) return { retry: false, alert: true };

  const reset = Number(headers["x-rate-limit-reset"]); // Unix seconds
  const waitMs = Math.max(reset * 1000 - Date.now(), 1_000);

  // Nothing here reports the daily cap, so treat a run of window-shaped
  // 429s on the same endpoint as the cap and hand the post back to the scheduler.
  return { retry: true, waitMs };
}
  • Window 429 → sleep until x-rate-limit-reset, retry once, give the slot back to the queue.
  • Daily cap 429 → reschedule. No retry budget beats a 24-hour window. We classify Instagram's equivalent daily publishing cap as explicitly non-retryable for the same reason.
  • usage-capped → zero retries, alert a human. It is a billing event wearing a rate limit's status code.
  • Duplicate content, the reply rule and the cashtag rule are terminal, kept as non-retryable patterns next to suspended accounts and expired tokens.

X's documentation does not mention a retry-after header for these endpoints and our X adapter does not read one, so the reset timestamp is what you have. The cross-platform version of this logic is in social media API error handling.

The version with one counter

Three counters, three resets, three reactions, and only one of them visible in a header. That is the maintenance cost of a direct integration, multiplied by every connected account.

bundle.social's X API collapses it. The 15-minute window is paced per account, the daily cap is counted on our side rather than discovered by hitting it, and the billing cap is handled before the request goes out: a hold on the wallet keyed to the post and the account, captured on success, released on failure. A publish retry reuses the existing hold instead of charging twice, and a post never leaves the queue when there is nothing to pay for it with. Error classification runs off the same type field described above, so a usage-capped 429 fails fast to a human instead of grinding through a retry budget.

If you publish to one account twice a day, none of this ever fires and a direct integration is less machinery to own. It starts mattering at fleet scale, where the per-user window, the per-app daily cap and the wallet all move independently and only one of them tells you where it stands.

Frequently asked questions

What is the rate limit for POST /2/tweets?

X's documentation gives three answers. The rate limits page says 100 per 15 minutes per user and 10,000 per 24 hours per app. The manage Posts page says 200 per 15 minutes per user, plus 300 per 3 hours shared with reposts. Plan against 300 per 3 hours, the tightest sustained rate.

What is the X API daily post limit?

For POST /2/tweets the documented per-app cap is 10,000 per 24 hours, alongside the per-user 15-minute window. No response header reports how much of the daily cap you have spent, so you count it yourself with a rolling 24-hour counter per endpoint.

Why does my X API 429 not go away after waiting?

Because it is probably not the 15-minute window. Check the type field: .../usage-capped means credits or the monthly read ceiling are gone, and waiting never clears it. A 24-hour cap also outlasts any backoff computed from x-rate-limit-reset, which only describes the short window.

What does x-rate-limit-remaining: 0 mean?

No requests left in the current window on that endpoint, for that auth context. It can appear on a successful 200, which makes it an early warning rather than an error. Treat it as already rate-limited and pause until x-rate-limit-reset instead of sending one more request to confirm.

Why does X reject my post as duplicate content?

X refuses identical text posted again from the same account. It documents neither the window nor how similar counts as identical, so treat it as terminal and change the text. It is also a signal after a timeout: a duplicate rejection usually means the first attempt published.

Marcel Czuryszkiewicz
Written by

Marcel Czuryszkiewicz

Co-Founder

Marcel is one of the builders behind bundle.social, creating a social media API with a strong focus on developer experience, reliability, and real support. Before bundle.social, he worked at Samsung R&D, Reply AI, and Docplanner, building internal tools that helped companies grow. He brought that same practical, product-focused mindset to bundle.social, so you can trust that your project is in good hands.

Keep reading