Rate limits
The real limits, what each call costs, the headers you get, and how to back off without making things worse.
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
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. metadata says which bucket refused it
(bucket: key, account, account-trades, ip…), the request’s weight, and
recommendedRetryTime, as on the /users/current/… API.
{ "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.
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
429with a new idempotency key. - Stream instead of polling. One WebSocket replaces hundreds of
GET /positionscalls, 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.