> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Twitter API rate limits, 429 errors & retry-after

> Understand Xquik Twitter API rate limits, recover from 429 errors, honor Retry-After, pace tweet and follower requests, and safely resume cursor exports.

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Xquik applies fixed-window Twitter API rate limits per Xquik account.
These limits protect tweet reads, profile lookups, writes, and deletions.
Standard API keys for one account share the same method buckets.
Separate read, write, and delete buckets reset independently.

<Note>
  **Standard limits.** 500 reads per second, 120 writes per minute, and 60
  deletes per minute. After `429`, wait for `Retry-After` before retrying.
</Note>

## Twitter API rate limits at a glance

| Rate-limit signal | Meaning | Client action |
| - | - | - |
| 500 reads per second | `GET`, `HEAD`, and `OPTIONS` share one account bucket. | Pace tweet, profile, follower, reply, and event reads below 500 requests per second. |
| 120 writes per minute | `POST`, `PUT`, and `PATCH` share one account bucket. | Queue tweets, monitors, webhooks, and profile changes below 120 requests per minute. |
| 60 deletes per minute | Every `DELETE` request uses the delete bucket. | Serialize cleanup work and keep each deleted resource ID. |
| `429 Too Many Requests` | An Xquik tier or action limit blocked the request. | Read the error code before choosing a retry delay. |
| `Retry-After` header | The response supplies a wait in seconds. | Pause that bucket for the supplied duration. |
| `retryAfter` or `retryAfterMs` | The JSON body supplies a retry duration. | Use the documented unit before resuming work. |

<CardGroup cols={3}>
  <Card title="Read bucket" icon="database">
    `GET`, `HEAD`, and `OPTIONS` allow 500 requests per 1 second.
  </Card>

  <Card title="Write bucket" icon="pen-line">
    `POST`, `PUT`, and `PATCH` allow 120 requests per 60 seconds.
  </Card>

  <Card title="Delete bucket" icon="circle-x">
    `DELETE` allows 60 requests per 60 seconds.
  </Card>
</CardGroup>

The listed request count succeeds inside each window. The next request receives
`429 rate_limit_exceeded`. A read burst does not consume write capacity.
A write burst does not consume delete capacity.

### Action-specific Twitter API limits

Some actions add a narrower bucket. That bucket applies beside the method tier.

| Action | Route | Additional limit | Recovery |
| - | - | - | - |
| Follow or remove follower | `POST /x/users/{id}/follow` and `POST /x/users/{id}/remove-follower` | 20 actions per minute and 400 per day, shared | Track both counts. Wait for `Retry-After` after `429`. |
| Connect X account | `POST /x/accounts` | 10 attempts per 15 minutes | Wait the returned remaining seconds. The maximum wait is 900 seconds. |
| Login cooldown | Account connection and reauthentication | Dynamic cooldown from the login attempt | Use `retryAfterMs` or `Retry-After`. |

The follow bucket protects connected X accounts from rapid automation.
It also covers follower removal. Reaching 400 actions blocks more attempts.
Track the daily count instead of retrying every minute.

## How the fixed window works

Xquik uses fixed-window counters. It does not use a token bucket.
The first counted request starts that bucket's window.

<Steps>
  <Step title="Start the window">
    The first request starts a 1-second or 60-second window.
  </Step>

  <Step title="Count every request">
    Each request increments its read, write, or delete counter.
  </Step>

  <Step title="Reject the overflow">
    Requests above the bucket limit receive `429 Too Many Requests`.
  </Step>

  <Step title="Reset the counter">
    The complete counter resets when its fixed window expires.
  </Step>
</Steps>

```text theme={null}
Read bucket: 500 requests per 1 second

0.000s  First GET starts the window
0.600s  Request 500 succeeds
0.700s  Request 501 returns 429 with Retry-After: 1
1.000s  The bucket resets
1.001s  The next GET succeeds
```

The window starts with your first request. It does not follow wall-clock seconds.
An early retry does not extend the existing window. It still returns `429`.
Always follow the response instead of guessing the reset time.

## Read a 429 rate-limit response

An Xquik tier limit returns a structured JSON error.
It also returns `Retry-After` in seconds.

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Try again later.",
  "retryAfter": 1
}
```

| Response field | Unit | Meaning |
| - | - | - |
| HTTP status `429` | None | A request limit or cooldown blocked the request. |
| `Retry-After` | Seconds | Minimum wait before retrying that operation. |
| `retryAfter` | Seconds | JSON copy of the Xquik tier wait. |
| `retryAfterMs` | Milliseconds | Login cooldown wait for account connection flows. |
| `error` | String | The exact reason, such as `rate_limit_exceeded`. |

Standard read throttles return `Retry-After: 1`. Standard write and delete
throttles return `Retry-After: 60`. Account connection returns the remaining
window. A login cooldown returns its own remaining duration.

### Retry-After does not always mean rate limited

Inspect the status and error code together.

| Status or code | Meaning | Correct action |
| - | - | - |
| `429 rate_limit_exceeded` | You exhausted the Xquik request bucket. | Wait for `Retry-After`, then retry once. |
| `429 x_rate_limited` | X throttled a connected account write. | Wait for `Retry-After` when present. Otherwise, back off. |
| `429 x_daily_limit` | The connected X account reached a daily write limit. | Wait 24 hours before reusing that account. |
| `422 x_automation_refused` | X refused a write that looked automated. | Send the next request now. Waiting does not help. |
| `429 login_cooldown` | A connection attempt triggered a cooldown. | Wait `retryAfterMs` or `Retry-After`. |
| `502 x_api_rate_limited` | The read service was throttled upstream. | Retry in a few minutes with bounded backoff. |
| `402 insufficient_credits` | The account cannot fund the requested results. | Add credits or request fewer tweets, followers, or replies. |
| `202` with `Retry-After` | A durable write remains active. | Poll `statusUrl`. Do not resend the write. |
| `503` with `Retry-After` | A temporary write or service state needs time. | Follow `safeToRetry`, `nextAction`, and the header. |

Read [API Error Handling](/guides/error-handling) before retrying writes.
An idempotency key prevents duplicate submission. It does not make every retry safe.

## Xquik limits versus official X API limits

The phrase "Twitter API limits" can describe 2 separate systems.
Xquik enforces the account buckets documented above. X also enforces upstream
limits for connected accounts and service access.

| Limit owner | Typical scope | Client-visible signal |
| - | - | - |
| Xquik | Account plus HTTP method tier | `429 rate_limit_exceeded` and `Retry-After` |
| Xquik action guard | Follow, follower removal, connection, or login | `429` with an action-specific error or cooldown |
| X write service | Connected X account and write action | `x_rate_limited` or `x_daily_limit` |
| X read service | Upstream tweet, profile, follower, or reply access | Default v1 can return `502 x_api_rate_limited` |

Official X API rate limits vary by endpoint and authentication context.
They can apply per app, user token, or endpoint.
See [X API rate limits](https://docs.x.com/x-api/fundamentals/rate-limits)
for the current official tables.

Do not copy official X limits into an Xquik client limiter.
Use the Xquik values on this page. Handle upstream codes separately.

## Recover from API rate limit exceeded

Use bounded retries for idempotent reads. Keep cursor state before waiting.
Never run an unbounded retry loop.

<Steps>
  <Step title="Classify the response">
    Check the HTTP status and exact `error` value.
  </Step>

  <Step title="Read the server delay">
    Prefer `Retry-After`. Fall back to `retryAfter` or `retryAfterMs`.
  </Step>

  <Step title="Keep the same read checkpoint">
    Keep the query, filters, limit, and cursor.
  </Step>

  <Step title="Wait with jitter">
    Add a small random delay after the required wait.
  </Step>

  <Step title="Retry a bounded number">
    Stop after 3 attempts. Return the final error to the caller.
  </Step>
</Steps>

### Node.js 429 recovery

This helper retries `GET` requests only. It does not retry writes.

```javascript theme={null}
const MAX_ATTEMPTS = 3;

function retryDelayMs(response, body) {
  const headerSeconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);

  if (Number.isFinite(headerSeconds) && headerSeconds > 0) {
    return headerSeconds * 1_000;
  }
  if (Number.isFinite(body.retryAfter) && body.retryAfter > 0) {
    return body.retryAfter * 1_000;
  }
  if (Number.isFinite(body.retryAfterMs) && body.retryAfterMs > 0) {
    return body.retryAfterMs;
  }
  return 1_000;
}

async function getJsonWithRateLimit(url) {
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
    const response = await fetch(url, {
      headers: { "x-api-key": process.env.XQUIK_API_KEY },
    });

    if (response.status !== 429) {
      if (!response.ok) {
        throw new Error(`Request failed with HTTP ${response.status}`);
      }
      return response.json();
    }

    const body = await response.json().catch(() => ({}));
    if (attempt === MAX_ATTEMPTS) {
      throw new Error(body.error ?? "rate_limit_exceeded");
    }

    const jitterMs = Math.floor(Math.random() * 250);
    const waitMs = retryDelayMs(response, body) + jitterMs;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }
}
```

The header wins when both values exist. Jitter spreads simultaneous workers.
The 3-attempt cap prevents a stalled queue from hiding an outage.

## Resume tweet exports after 429

Tweet searches return `has_next_page` and `next_cursor`.
Store the completed page and next cursor atomically.
Do not advance the cursor after a failed request.

```javascript theme={null}
const baseUrl = "https://xquik.com/api/v1/x/tweets/search";
let cursor;

while (true) {
  const url = new URL(baseUrl);
  url.searchParams.set("q", "from:example launch");
  url.searchParams.set("queryType", "Latest");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const page = await getJsonWithRateLimit(url);

  // Commit unique tweets[].id values and next_cursor together.
  await storeTweetPageAndCursor(page.tweets, page.next_cursor);

  if (!page.has_next_page || !page.next_cursor) break;
  cursor = page.next_cursor;
}
```

Keep `q`, `queryType`, filters, and `limit` unchanged while resuming.
Deduplicate stored tweets by `tweets[].id`. A repeated page then remains harmless.
See [Request-Efficient API Usage](/guides/request-efficient-api-usage#store-cursor-checkpoints)
for more checkpoint patterns.

### Cursor recovery checklist

| Checkpoint | Save after success | Reuse after 429 |
| - | - | - |
| Search query | Exact `q` string | Yes |
| Sort order | Exact `queryType` | Yes |
| Filters | Author, date, media, language, and engagement filters | Yes |
| Page size | Exact `limit` | Yes |
| Incoming cursor | Cursor used for the blocked page | Yes |
| Returned cursor | `next_cursor` from the completed page | Only after storing that page |
| Tweet identity | Every `tweets[].id` | Use for deduplication |

## Pace requests before 429

Leave headroom below each documented limit. Headroom absorbs network timing
and work from other API keys. It also protects shared serverless workers.

### Use a shared Node.js limiter

This Bottleneck configuration reserves 10% read headroom.

```javascript theme={null}
import Bottleneck from "bottleneck";

const readLimiter = new Bottleneck({
  reservoir: 450,
  reservoirRefreshAmount: 450,
  reservoirRefreshInterval: 1_000,
  maxConcurrent: 5,
});

const response = await readLimiter.schedule(() =>
  fetch("https://xquik.com/api/v1/x/tweets/search?q=launch&limit=100", {
    headers: { "x-api-key": process.env.XQUIK_API_KEY },
  }),
);
```

One in-memory limiter coordinates only one process.
Use a shared queue across multiple servers, functions, or containers.

### Rate-limiting libraries

<CardGroup cols={3}>
  <Card title="Node.js libraries" icon="package">
    Use [bottleneck](https://github.com/SGrondin/bottleneck) for shared queues,
    or [p-limit](https://github.com/sindresorhus/p-limit) for concurrency caps.
  </Card>

  <Card title="Python library" icon="package">
    Use [ratelimit](https://github.com/tomasbasham/ratelimit) with
    `pip install ratelimit`.
  </Card>

  <Card title="Go library" icon="package">
    Use [rate](https://pkg.go.dev/golang.org/x/time/rate) with
    `go get golang.org/x/time/rate`.
  </Card>
</CardGroup>

A client scheduler can use another algorithm. Keep its output below Xquik's
fixed-window limits. Configure one coordinator per Xquik account.

## Reduce Twitter API requests

The limit counts requests, not returned tweets or followers.
Use each endpoint's largest suitable page size.
One 100-tweet page uses one read request.
One request per tweet would use 100 read requests.

<AccordionGroup>
  <Accordion title="Use cursor pagination">
    Request a full page. Store `next_cursor`, then continue the same query.
    Never restart from page one after a recoverable `429`.
  </Accordion>

  <Accordion title="Batch IDs where supported">
    Batch tweet or profile IDs through the documented batch endpoints.
    One batch request uses fewer read slots than individual lookups.
  </Accordion>

  <Accordion title="Use webhooks instead of polling">
    Let monitors deliver matching tweets through signed webhooks.
    Use event reads for backfills, reconciliation, and missed delivery checks.
  </Accordion>

  <Accordion title="Cache stable profile fields">
    Cache names, usernames, profile images, and account settings.
    Refresh them on a schedule that matches your product needs.
  </Accordion>

  <Accordion title="Centralize worker concurrency">
    Multiple API keys do not multiply a standard account's limits.
    Route every worker through one account-aware queue.
  </Accordion>

  <Accordion title="Separate read and write queues">
    Use different queues for reads, writes, and deletes.
    This matches the independent server buckets.
  </Accordion>
</AccordionGroup>

## Monitor API throttling

Record enough context to explain each `429`. Never log API key values.

| Metric or log field | Why it matters |
| - | - |
| HTTP method and route | Identifies the exhausted method bucket. |
| `error` code | Separates Xquik limits from upstream X throttling. |
| `Retry-After` | Confirms the required pause. |
| Attempt count | Detects unbounded retry behavior. |
| Worker concurrency | Shows whether parallel jobs share excessive load. |
| Query and cursor hash | Connects a retry to the same tweet export page. |
| Completed tweet count | Confirms progress before throttling. |

A rising Xquik `429` rate usually means excessive local concurrency.
A rising `x_api_rate_limited` count indicates a different upstream condition.
Keep those alerts separate.

## Twitter API rate-limit questions

### What does API rate limit exceeded mean?

The client sent more requests than one active window allows.
Xquik returns `429 rate_limit_exceeded` for its own exhausted bucket.
Wait for `Retry-After`, then retry the same idempotent read.

### How long does a Twitter API rate limit last?

Xquik read windows last 1 second. Write and delete windows last 60 seconds.
Connection safety windows last 15 minutes. Login cooldowns use dynamic waits.
Official X endpoint windows differ from these Xquik limits.

### Why am I rate limited below 500 reads?

All standard keys for one Xquik account share the read bucket.
Another worker, function, or server may consume the remaining requests.
Centralize scheduling and reserve headroom below 500 requests per second.

### Do multiple API keys increase my rate limit?

No. Standard API keys resolve to the same Xquik account buckets.
Use separate keys for access control, rotation, and auditability.
Do not use them to bypass request limits.

### Does retrying early reset the window?

No. An early retry does not extend or reset the fixed window.
It still returns `429` until the original window expires.

### Does a rate-limited request consume tweet credits?

An Xquik tier rejection happens before the endpoint performs its work.
It does not collect new tweets, followers, profiles, or replies.
Successful pages completed before the rejection keep their normal charges.

### Should I retry a tweet write after 429?

First inspect `error`, `statusUrl`, `terminal`, and `safeToRetry`.
Poll an active durable action instead of resending it.
Use a new idempotency key only when `safeToRetry` is `true`.

### How do I avoid Twitter API rate limits?

Use larger pages, cursor checkpoints, batch routes, caches, and signed webhooks.
Share one limiter across workers. Keep separate queues for each method bucket.

<CardGroup cols={3}>
  <Card title="Search tweets" icon="search" href="/api-reference/x/search-tweets">
    Search tweets with filters, page limits, and cursor recovery.
  </Card>

  <Card title="Error handling" icon="triangle-alert" href="/guides/error-handling">
    Classify billing, validation, dependency, and write lifecycle errors.
  </Card>

  <Card title="Pagination" icon="list" href="/guides/request-efficient-api-usage#store-cursor-checkpoints">
    Keep tweet, follower, reply, and extraction cursors safely.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.