DOCS
Limits & errors
What counts against you, what doesn't, and how errors are shaped.
Streaming limits
- Messages are unlimited on all paid plans — there are no per-tick fees or overage charges.
- Symbols and connections are capped per package, and the cap is what your futures package sets:
| Package | Symbols | Connections |
|---|---|---|
| Free / delayed (no subscription) | 10 | 1 |
| Realtime ($29) | 25 | 2 |
| Realtime + L2 ($79) | unlimited | 3 |
| L3 / Market-by-order ($199), Desk | unlimited | 10 |
| Legacy plans (Realtime, Pro, Ultra before the package change) | unlimited | unlimited |
- Symbols are counted across all channels on one connection, not per channel.
NQ ticks + NQ book + NQ L3 is three of your allowance, because each one is an upstream
subscription we pay for. Over the cap, the subscription is refused with a
symbol_limit_reachedframe naming the symbol — the ones you already hold keep running, and nothing is dropped silently. - An options package does not raise your streaming allowance. Options Core, Flow and Pro buy options data; they grant no symbols and no connections. An options-only key streams on the free tier's 10 symbols and 1 connection. Need more, add a futures package.
- Allowances do not add up. If you hold several products, the most generous one wins — Realtime plus Realtime + L2 is unlimited symbols and 3 connections, not 5.
- A connection over your limit is closed with
too_many_connections(WebSocket close code4029), carrying the number you hold and the number allowed.
REST rate limits
There is currently no hard per-key REST rate limit, and no X-RateLimit-*
headers. This page used to publish 60 requests/minute with those headers; a customer
asked us to confirm it on 17 August 2026 and it turned out to be documentation for something
that was never built. We would rather correct the page than let you design a backoff around a
number that does not exist.
What is actually true today:
- Live connections are capped, not requests. Your package's connection count is the real limit — see your plan on pricing.
- Options history runs one request at a time per key.
/v1/options/*history calls (eod,oi,trade_greeks,trade_quote…) and historical GEX are queued: each key has one in flight, further ones wait, and keys take turns. A request that has waited 60 s is answered503 history_busywith aRetry-Afterheader — nothing is wrong with the request, retry after that many seconds. For bulk backfills send one request at a time and useexpiration=*per day; firing in parallel only makes your own requests wait longer. - Please still be reasonable. REST is for snapshots and backfill — stream what you need continuously over the WebSocket instead of polling
/quotein a tight loop. If we ever do need a hard limit, it will be published here first, with the headers to match.
Error shape
All errors share the same JSON envelope with a stable code; some add fields that help you act on it:
{
"error": {
"code": "request_type_not_in_plan",
"status": 403,
"message": "'trade_quote' needs Options Pro ($119/mo)",
"your_request_types": ["chain", "…"]
}
} Status codes
The status says what kind of problem it is; the code says which one. Branch on the code.
| Status | Codes | Meaning |
|---|---|---|
400 | missing_symbol, bad_start, bad_end, bad_range, window_too_large, contract_not_addressable, unknown_parameter (on /gex), bad_request | The request itself is wrong; the message names the parameter. |
401 | unauthorized | Missing or invalid API key. |
403 | options_plan_required, request_type_not_in_plan, gex_plan_required, data_plan_required, plan_required, account_suspended | Your packages don't include this. The message names the package that would. |
404 | no_data, unknown_symbol (on /cot) | Nothing for that symbol or window. /ticks answers an empty window with 200 and count: 0 instead. |
429 | rate_limited | Only the Paste Pack's per-board pacing on /gex; there is no general REST rate limit. |
5xx | read_failed, *_unavailable, chain_loading | Our side or our upstream. Retry with backoff; we're paged. chain_loading is not a fault: that option chain is being added to the live poll — retry after the Retry-After seconds. history_busy (503) is the options-history queue: retry after Retry-After. |
The SDKs raise typed exceptions/errors carrying the code, so you can branch on it without parsing strings.