How to calculate load on the Nado gateway, which limits are published, and how to configure a local throttle. All exchange limits here are from documentation (docs.nado.xyz, developer-resources/api/rate-limits): they were not verified by hitting the limit live. Headroom, burst, and concurrency depend on the workload and are configured by the calling application.
TL;DR
- Two documented limit families: queries — 2400 weight/min per IP (burst 400 per 10 s); executes — 600/min per wallet (burst 100 per 10 s). A second wallet expands only the execute budget; a second IP expands only the query budget. → §1
- Local throttle configuration is mandatory: sustained rate and burst are configured separately for queries and executes, along with shared HTTP concurrency.
nado-kitdoes not insert author-selected values. → §3 - The expensive query is
orders: weight 2 per product id. Chunk size must be explicit and keep request weight within the configured query burst; otherwise, the request is impossible by construction. → §4 - Executes are counted separately: replacing an order costs 2 executes (cancel + place), and rejected repeats also consume budget. Action frequency must be calculated against the explicitly selected execute rate. → §5
- Retry by error class: do not retry a failure envelope; retry 429; retry transport/timeout only for idempotent operations. Every request has a 10-second timeout. → §6
1. Documented limits
| Limit | Key | Value | Burst |
|---|---|---|---|
| queries | IP | 2400 weight/min | 400 / 10 s |
| executes | wallet | 600 weight/min | 100 / 10 s |
Not verified live: which HTTP code and body are returned when exceeded (presumably 429), window length, whether executes are counted against the master wallet or the signer, and whether multiple sub-accounts under one address share an execute budget.
2. Request weights
Query weights are from documentation (developer-resources/api/rate-limits), as encoded in the local throttle. Execute weights (1 for place_order / cancel_orders, 30 for link_signer) are the values encoded in the throttle; they were not independently checked against documentation.
| Weight | Query / execute |
|---|---|
| 1 | contracts, order, market_price (one product); execute place_order, cancel_orders |
| 2 | symbols, subaccount_info, nonces, subaccount_orders; orders — 2 per product id; market_prices — ≈ 1 per product id |
| 5 | linked_signer, all_products |
| 30 | execute link_signer (in the local throttle) |
The weight of fee_rates is not recorded.
3. Local throttle
Mechanics: token bucket by weight + concurrency semaphore + retry by error class (§6), with separate buckets for queries and executes. createWeightThrottle requires five explicit parameters: queriesPerMinute, queryBurst, executesPerMinute, executeBurst, maxConcurrent.
Rules:
- Give the execute queue high priority so background reads do not delay a cancellation or close.
- Periodically flush labeled call counters (
orders,subaccount_info,order:close, …) to logs: this is the only way to see which subsystem consumes the budget. - The selected sustained rate and burst must account for documented ceilings, other processes using the same IP/wallet, and the exchange’s unknown actual window.
A local throttle refusal ≠ an exchange rejection. Conflating them suppresses real exchange rejections and produces false alarms that clear by themselves seconds later. A local-window refusal is temporary (it clears within seconds); an exchange minimum rejection is permanent. Their statuses must be distinct.
4. Cost of one tick
For one sub-account with a working set of N products and a positions → orders → positions fence:
| Operation | Weight | Notes |
|---|---|---|
subaccount_info × 2 | 4 | fence |
orders for the working set | 2 × N | explicit chunk size that fits within query burst |
market_prices for the working set | ≈ N | mids |
symbols | 2 per request | frequency is controlled by the caller’s cache |
complete orders survey | 2 × product count | only when a completeness consumer exists |
IOC fill: subaccount_info before and up to 3 after | 2 + 2..6 | per IOC |
Total per-minute consumption is the weight of one pass multiplied by pass frequency. Compare it with explicit configuration, not with a universal “tick budget.”
Bucket-capacity trap. A request whose weight exceeds the configured capacity can never obtain tokens. The implementation rejects it with RangeError; therefore, chunkProductIds and readOpenOrders require an explicit chunk size.
5. Executes: where the budget leaks
- Replacing one order costs 2 executes (cancel + place).
- If your resting orders for an asset are canceled before an IOC on that asset (for example, a TP that could reverse the position because resting reduceOnly does not exist,
orders.md§3), the IOC also incurs the cancellations and subsequent replacement. - Closing on a
post_onlymarket: IOC is rejected with 2117; repeated attempts continue consuming execute budget. Retry only after a fresh market-mode read, and keep its frequency within the configured execute rate. - Nado has no separate hard write window beyond the execute limit. Still calculate the action cost before the first cancellation.
6. Retries
| Class | Retry | Why |
|---|---|---|
failure envelope with error_code | no | the sequencer responded and the order was not applied; each code has its own branch |
| HTTP 429 | yes, backoff + jitter | rejected before matching |
| 5xx, timeout (10 s), disconnect, non-JSON | query — yes; execute — only idempotent operations (cancel_orders, full reduceOnly close) | outcome is unknown: an entry or partial reduction may have been applied |
An execute is marked with an idempotency flag when sent; the retry wrapper decides from that flag. Retrying a non-idempotent execute after a timeout risks a double position.
7. Pitfalls
| What breaks | Why | Correct approach |
|---|---|---|
An orders request is rejected locally | its weight exceeds the configured bucket capacity | choose an explicit chunk size that fits within query burst |
| Read budget is consumed by mids | market_prices for all markets on every tick | use the working set; query the entire universe only when the set is empty |
| A close waits behind background reads | one priority | execute is high priority; the semaphore admits it first |
| False “orders are not being placed” alarm that clears seconds later | a local throttle refusal was treated as an exchange rejection | distinct statuses for a local throttle refusal and an exchange minimum rejection |
| Execute budget is consumed on weekends | close rejected with 2117 is retried every tick | calculate cost before acting; make a second close attempt only after a fresh exchange signal |
8. Open questions / not verified
- All exchange-limit numbers come from documentation: 2400/min per IP and 600/min per wallet were not verified by hitting the limit live; rejection code, body, and window were not captured.
- Which wallet executes are counted against—the master or linked signer; whether sub-accounts under one address share a budget.
ordersweight = 2 × product count is encoded from documentation; actual exchange accounting was not verified.- Direct hosts: whether their limits are separate or shared with the gateway is unknown.
- WebSocket as a polling replacement, if available, has not been studied; if one appears, recalculate §4.
- Actual labeled consumption was not measured: the numbers in §4 are calculations from weights.
Facts verified through 2026-09-16. Nado limits come from documentation; recheck them and rejection shapes with live requests.
© markpaper authors. Licensed under CC BY 4.0: when publishing or adapting this material, credit “markpaper — Nado knowledge base” and link to the original and the license.