# Rate limits

> The real limits, what each call costs, the headers you get, and how to back off without making things worse.

Source: https://tickwire.io/docs/rate-limits/

Tickwire uses a few plain token buckets, not a credit system. Each request costs a small, fixed
number of **units** (its weight), and each bucket refills continuously. Limits apply per API key and
per trading account, so one noisy integration can't starve your others, and trades have their own
budget, so polling can never block an order.

## The limits

| Bucket | Burst (units) | Refill (units per second) | Applies to |
|---|---|---|---|
| Per key | 100 | 50 | Every authenticated request, by API key or token |
| Per account | 40 | 20 | Requests that name a trading account (all keys together) |
| Per account, trades | 20 | 10 | Trades on one account (all keys together) |
| Per IP, anonymous | 20 | 5 | Requests without a key (sign-up, public lists) |
| Per IP, failed auth | 20 | 0.5 | Requests with an invalid key |
| Per IP, MCP server | 200 | 50 | The hosted MCP server (it also limits per key) |

A request passes only if **every** bucket it uses has room, and then all of them are charged. A
refused request charges nothing.

Streaming:

| Limit | Value |
|---|---|
| Concurrent WebSockets per key | 50 |
| Request body | 16 MiB |
| WebSocket message | 16 MiB |

## What each call costs

| Class | Weight | Buckets | Calls |
|---|---|---|---|
| Standard | 1 | key + account | Reads and account changes: positions, orders, symbols, prices, account info |
| History | 5 | key + account | History and bulk reads: `history-orders`, `history-deals`, `historical-market-data`, `historical-trades`, terminal `OrderHistory`, `PositionHistory`, `PriceHistory` |
| Trade | 1 | key + account trades | `POST /users/current/…/trade`, terminal `OrderSend`, `OrderModify`, `OrderClose`, `CloseAll`, `POST /v1/execution/accounts/{id}/trade` |
| Batch orders | 10 | key | `POST /v1/orders`, `POST /v1/orders/close` (each leg is also checked by the account) |
| Stream connect | 5 | key + account | Opening a WebSocket. Frames on an open socket are free |

So with one key you can, at a steady rate, make 50 reads a second, or 10 history calls a second, or
trade 10 times a second on each account, and bursts of twice that.

## Trades are never dropped silently

A trade refused by a rate limit is answered `429` with `"sent": false` and `"outcome": "not_sent"`.
It never reached the terminal and was not queued. You can resend it after `Retry-After`; use the same
`Idempotency-Key` anyway, as for every trade.

## Headers

Every response carries the tightest bucket's state, in the IETF
[RateLimit header fields](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)
form, plus the same numbers under the older `X-` names:

| Header | Meaning |
|---|---|
| `RateLimit-Limit` | The bucket's burst, in units |
| `RateLimit-Remaining` | Units left right now |
| `RateLimit-Reset` | Seconds until the bucket is full again |
| `RateLimit-Policy` | `<burst>;w=<seconds to refill from empty>;name="<bucket>"`, e.g. `100;w=2;name="key"` |
| `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` | The same values |

A `429` also carries `Retry-After` (seconds until this request would pass) and the standard error
envelope with code [`rate_limited`](https://tickwire.io/docs/errors/rate_limited/). `metadata` says which bucket refused it
(`bucket`: `key`, `account`, `account-trades`, `ip`…), the request's `weight`, and
`recommendedRetryTime`, as on the `/users/current/…` API.

```json
{ "error": "rate_limited", "message": "too many requests; retry after the time in Retry-After",
  "sent": false, "outcome": "not_sent",
  "metadata": { "recommendedRetryTime": "2026-09-25T11:02:20.000Z", "exceededConfig": "perAccountTrades",
                "resourceId": "account:acc_7Qx", "limit": 20, "bucket": "account-trades", "weight": 1 },
  "requestId": "4f1c…", "docUrl": "https://tickwire.io/docs/errors/rate_limited" }
```

On the `/users/current/…` routes `error` is `TooManyRequestsError` and our code is in `code`, so
existing clients that read `error` handle it as they do today. Too many open WebSockets is `429`
[`too_many_connections`](https://tickwire.io/docs/errors/too_many_connections/).

## Backing off well

- Wait at least `Retry-After`. Then back off exponentially with **jitter**, capped at 30 seconds or
  so. Without jitter, every client retries at the same instant and trips the limit again.
- Never auto-retry a trade on `429` with a **new** idempotency key.
- Stream instead of polling. One WebSocket replaces hundreds of `GET /positions` calls, and its frames
  don't count against request limits.

## What doesn't count

Webhook deliveries, WebSocket frames we send you, and the health checks `/_gateway/healthz` and
`/_gateway/readyz`.

Need more? Higher limits are available on request, per key; the headers always show the limits that
apply to you.
