Last updated: August 2026

X API rate limit 429: what triggers it and how to back off

A 429 means a rate limit window is empty, not that your credentials are wrong. X counts separately per app, per user, per endpoint, and against a 24 hour write cap. Read x-rate-limit-reset, a Unix timestamp in seconds, and sleep until it. Do not guess with blind exponential backoff.

Below: the four buckets that can empty independently, the six headers worth reading, a retry loop that respects them, and why your request count looks lower than X thinks it is.

Four buckets, any one of which returns 429

The most common confusion about X rate limits is treating them as one number. There are several counters running at once, and a 429 tells you one of them hit zero without telling you which.

Bucket
What it counts
Why it catches people out
Per app
Every request your app makes, across all users and all processes
The one people forget. A second worker or a local dev run drains the same allowance.
Per authenticated user
Requests made on behalf of one user token
Independent of the app bucket. You can exhaust either one on its own.
Per endpoint
Each endpoint keeps its own window
A 429 on POST /2/tweets says nothing about your remaining reads.
24 hour write cap
A rolling daily ceiling on posts for the user
Also returns 429, but its reset can be hours away, not minutes.

Do not hardcode the numeric limits

X has changed the per-window numbers repeatedly, and they differ by endpoint and by access level. Any figure you paste into a constant will be wrong at some point without your code noticing. Read the headers on every response and drive your throttling from them. That is the only version that stays correct.

The six headers to read

X returns these on successful responses too, not only on the 429. That is the useful part: you can slow down before you get blocked rather than after.

x-rate-limit-limitHow many requests the current window allows.
x-rate-limit-remainingHow many are left in the current window. Throttle yourself before this reaches zero.
x-rate-limit-resetA Unix timestamp in seconds, not a duration. Sleep until this, then retry.
x-user-limit-24hour-limitThe daily write ceiling for this user.
x-user-limit-24hour-remainingWrites left today. Zero here means waiting hours, not seconds.
x-user-limit-24hour-resetUnix timestamp for when the daily write ceiling clears.

x-rate-limit-reset is a timestamp, not a delay

Treating it as a number of seconds to wait is a common bug. The value is Unix epoch seconds. If you sleep for that many seconds you will pause for decades, and if your library silently coerces it you will get a retry that never fires.

Correct backoff for the X API

Generic advice says exponential backoff with jitter. That is right when a service tells you nothing. X tells you the exact second the window clears, so the correct behaviour is to sleep until that second. Exponential backoff is the fallback for when the header is absent, and jitter still matters there because several of your workers will wake up at once.

Two rules that prevent most of the damage: only retry 429 and 5xx, and always cap total attempts. A 403 will never succeed on retry, and retrying it can create duplicate content errors, which is covered in the X API 403 guide.

backoff.ts
const MAX_ATTEMPTS = 5;

async function callX(req: Request) {
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
    const res = await fetch(req);
    if (res.status !== 429) return res; // 403 and 400 are terminal, hand them back

    const waitMs = waitFromHeaders(res.headers) ?? backoffWithJitter(attempt);
    if (waitMs > 15 * 60_000) throw new Error('daily cap hit, resume at reset');
    await sleep(waitMs);
  }
  throw new Error('rate limited after ' + MAX_ATTEMPTS + ' attempts');
}

function waitFromHeaders(h: Headers): number | null {
  // The daily write cap wins: its reset can be hours out.
  const daily = h.get('x-user-limit-24hour-remaining');
  const key = daily === '0' ? 'x-user-limit-24hour-reset' : 'x-rate-limit-reset';
  const reset = h.get(key);
  if (!reset) return null;
  return Math.max(0, Number(reset) * 1000 - Date.now()) + 1_000; // epoch seconds, not a delay
}

function backoffWithJitter(attempt: number): number {
  const base = Math.min(60_000, 1_000 * 2 ** attempt);
  return base / 2 + Math.random() * (base / 2);
}

Never sit in a tight loop on the daily cap

If x-user-limit-24hour-remaining is zero, the window may not clear for hours. A loop that retries every few seconds will make thousands of pointless requests and, under pay-per-use billing, some of those failures still cost you. Detect the daily cap, stop, and schedule a resume at the reset timestamp instead.

Why your request count looks lower than X thinks it is

The complaint is almost always the same: barely any traffic, constant 429s. Four usual explanations, in the order they turn out to be true.

  • The app bucket is shared. Production, staging, and the copy running on your laptop all authenticate as the same app and drain the same allowance.
  • Your retry logic is the traffic. A failing call retried five times is five requests. During an outage that multiplies across every queued job at once.
  • A read is not one unit. A timeline or search request can return many posts. Under pay-per-use it bills per post read, so a small number of requests can be a large amount of work.
  • You are looking at the wrong counter. If the 429 came with x-user-limit-24hour-remaining at zero, the 15 minute window was never the problem.

Log the three x-rate-limit values on every response for a day. The graph answers this faster than any amount of reasoning about your own code, and it usually shows a second process you had forgotten about.

One more thing worth checking while you are in there. Since pay-per-use billing began, a request that fails still represents work you asked for, and a retry storm is expensive as well as throttled. The per-call rates are in X API pay-per-use explained, and the ceilings that bound the damage are in does X API pay-per-use have a spend cap.

If you are only posting, the limits get simpler

Most of this complexity exists because you are running your own X app and carrying its buckets. If your workload is publishing rather than research, you can move the X-side limits to a service that manages them and deal with one plain per-minute allowance instead.

On OpenTweet that allowance is 60 requests a minute on Pro, 300 on Advanced, and 600 on Agency. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 includes Retry-After in seconds, so the retry logic is three lines rather than the function above. The details are in the rate limits doc, and the endpoint reference is in the API docs.

retry.sh
# On 429, OpenTweet returns Retry-After in plain seconds.
curl -i -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{"text":"Queued from a worker.","publish_now":true}'

# X-RateLimit-Limit: 60
# X-RateLimit-Remaining: 59
# X-RateLimit-Reset: 1756636800

7-day free trial. Cancel anytime.

Frequently asked questions

What does a 429 from the X API mean?

It means you exceeded a rate limit window, not that anything is wrong with your credentials. X counts requests in separate buckets: per app, per authenticated user, and per endpoint, plus a rolling 24 hour cap on writes. Any one of those buckets emptying returns 429 while the others may still have room.

Which headers tell me when I can retry the X API?

Read x-rate-limit-limit, x-rate-limit-remaining and x-rate-limit-reset. The reset value is a Unix timestamp in seconds, not a duration. For write endpoints also read x-user-limit-24hour-limit, x-user-limit-24hour-remaining and x-user-limit-24hour-reset, which track the separate daily posting cap.

Should I use exponential backoff for X API 429s?

Not on its own. X tells you the exact second the window resets, so sleeping until x-rate-limit-reset is both correct and faster than guessing. Use exponential backoff with jitter only as a fallback when the header is missing, and only for 429 and 5xx. Never retry a 403 or a 400.

Why do I get 429 on the X API when I have barely made any requests?

Usually because the bucket is shared. Rate limits count per app across every user and every process, so a second worker, a cron job, a local dev run, or a retry storm elsewhere in your system drains the same allowance. Check total requests from the app, not requests from the piece of code you are looking at.

Does the X API 24 hour tweet cap return 429 too?

Yes. The daily write cap surfaces as a 429 with x-user-limit-24hour-remaining at zero, and its reset can be many hours away. That is why a retry loop that ignores headers is dangerous here: it will hammer a limit that will not clear for hours instead of sleeping until the value in x-user-limit-24hour-reset.

How do I stop hitting X API rate limits when posting?

Reduce writes, or move posting off your own X app. If you post through OpenTweet, the X-side limits are handled by the service, and your own allowance is a plain per-minute number: 60 requests a minute on Pro, 300 on Advanced, 600 on Agency, returned in X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers with a Retry-After on 429.

Post to X without carrying X rate limit buckets

Flat $11.99 a month, one bearer key, plain per-minute limits with Retry-After. No X developer app.

7-day free trial. Cancel anytime.