# Rate limits: tiers, headers and backing off

> okdata rate limits per account: requests a minute, calls in progress and daily allowances per tier, the X-RateLimit headers, rate_limited with retry_after_seconds, and backoff code.

Canonical page: https://okdata.online/docs/rate-limits/

Limits belong to the account, not the key: every API key of an account, and every MCP `call` made with them, draws on the same counts. A new account can send 60 requests a minute; from the first top-up it can send 600. Past a limit you get `429 rate_limited` with `retry_after_seconds`, and nothing is charged.

## The tiers

| Tier | When | Requests a minute | Calls in progress | Daily spending allowance | not_found answers a day |
| --- | --- | --- | --- | --- | --- |
| trial | until the first top-up | 60 | 5 | $5 | 200 |
| standard | from the first top-up | 600 | 20 | $1,000 | 5,000 |
| pro | set by us on request | 3,000 | 50 | $10,000 | 20,000 |

`GET https://api.okdata.online/v1/account` shows your tier under `rate`, with `per_minute`, `concurrent` and `free_per_minute`. It is free.

## Requests a minute: burst, then refill

The per-minute count is a token bucket. It starts full, so you may send the whole minute's allowance at once, and it refills steadily: one request a second on trial, ten on standard, fifty on pro. A script that sends 600 requests in a burst on standard must then wait for the refill; one that keeps to ten a second never hits the limit.

Every keyed request takes from the count, including one refused for a bad parameter. The free routes, `/v1/account`, `/v1/calls` and `/v1/usage`, have their own count of 120 a minute on every tier, so checking a balance or a receipt never eats into your calls. The catalog and status routes need no key.

## Headers

Every answer to a request with a valid key carries:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | The per-minute count of the route |
| `X-RateLimit-Remaining` | Whole requests you can send right now |
| `X-RateLimit-Reset` | When the count is full again, in Unix seconds |
| `Retry-After` | Seconds to wait; sent with `rate_limited` and with any error that has a `retry_after_seconds` |

```bash
curl -s -D - -o /dev/null "https://api.okdata.online/v1/douyin/trends/list" -H "Authorization: Bearer $OKDATA_API_KEY"
```

```
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1794470460
```

That request is a real call and costs $0.002. Watch `X-RateLimit-Remaining` to slow down before you are refused.

## When you are refused

```json
{
  "code": "rate_limited",
  "request_id": "0f6c2a8e-4b1d-4e0a-9a51-3c7d2b9e1f20",
  "message": "Too many requests, or too many calls in progress",
  "message_zh": "请求太多，或同时进行的调用太多",
  "charged": false,
  "retry": "yes",
  "retry_after_seconds": 1,
  "param": null,
  "problems": ["Too many requests for this account (600 a minute); retry in 1 s."],
  "reason": "rate",
  "docs_url": "https://okdata.online/docs/errors/#rate_limited"
}
```

- `reason: rate`: past the per-minute count. Wait `retry_after_seconds`, which is also the `Retry-After` header.
- `reason: concurrency`: the account already has as many calls running as its tier allows. `retry_after_seconds` is null; retry when one of your calls finishes.
- `cost_cap_exceeded`: the account's charges today plus what this call could cost would pass the daily spending allowance.
- `uncharged_cost_limit`: the account has had as many `not_found` answers today as its tier allows.

Both allowances open again at 00:00 UTC, and their `retry_after_seconds` counts down to it, so do not sleep on them in a loop. Every one of these is `429` with `charged: false`. The [errors page](/docs/errors/) has the full list.

## Backing off

Retry `rate_limited` only. Wait `retry_after_seconds` when it is given, back off exponentially when it is null, and add a little jitter so parallel workers do not retry in step. Keep the same `Idempotency-Key` for every attempt so a retry can never be billed twice:

```python
import os
import random
import time
import uuid
import requests

URL = "https://api.okdata.online/v1/xiaohongshu/notes/search"
HEADERS = {"Authorization": f"Bearer {os.environ['OKDATA_API_KEY']}"}

def search(query, attempts=5):
    headers = {**HEADERS, "Idempotency-Key": str(uuid.uuid4())}
    for attempt in range(attempts):
        body = requests.get(URL, headers=headers, params={"query": query}, timeout=60).json()
        if body["code"] != "rate_limited":
            return body
        wait = body["retry_after_seconds"] or min(2 ** attempt, 30)
        time.sleep(wait + random.uniform(0, 0.5))
    raise RuntimeError("still rate limited; slow down or ask for a higher tier")

print(search("sunscreen")["meta"]["billing"])
```

For steady volume, pacing beats retrying: on standard, ten requests a second with at most 20 in flight stays inside the per-minute and in-progress limits.

## MCP shares the limit

The MCP server at `https://api.okdata.online/mcp` uses the same key, so its `call` tool takes from the same per-minute count and the same calls-in-progress slots as your HTTP calls, and `account` takes from it when a key is sent. `find` and `describe` need no key and are not counted. An agent and a script on one account therefore share one budget: a busy agent can slow a script down, and the other way round.

## Asking for more

The pro tier, or limits that suit your workload, are set by hand. Write to hello@okdata.online with your account ID (from `GET https://api.okdata.online/v1/account`), the endpoints you call and the peak rate you expect. How keys and accounts relate is on the [authentication page](/docs/authentication/).

Updated 2026-11-12
