Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Rate limits & quotas

What this teaches: the two server-side caps that affect Python SDK users, and how to stay under them.

The two limits

LimitSurfaceWhat it caps
167 requests / 10 sHTTP /graphqlAll POST queries and mutations from one IP
30 subscriptions / connectionWebSocket /graphqlSimultaneous subscribe frames on one socket

Both limits are enforced server-side. Hitting the HTTP limit returns 429 with a ClientError; hitting the WebSocket limit returns an error frame on the subscription, which surfaces as {"_error": ...} in your callback.

HTTP: 167 / 10 s

Bursting is fine within the window; sustained traffic above 167 req / 10 s gets throttled. Common ways to blow the limit:

  • Calling pair_param(pair_id) in a tight loop across 50 pairs instead of pair_params() once.
  • Using query_app_multi is the right answer for atomic batched reads.
  • paginate_all over a high-cardinality user's events without a page_size ceiling.
# Wasteful: 50 round trips
for pair_id in pairs:
    info.pair_param(pair_id)
 
# Efficient: one round trip
all_params = info.pair_params()

The SDK does not retry

There is no built-in retry, no jittered backoff, no circuit breaker. The choice is intentional — callers know their own throughput and tolerance better than a library default would. Wrap calls yourself:

import time
from dango.utils.error import ClientError, ServerError
 
def with_backoff(call, max_retries=5):
    for attempt in range(max_retries):
        try:
            return call()
        except ServerError:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)
        except ClientError as exc:
            if "429" not in str(exc) or attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)
 
balances = with_backoff(lambda: info.user_state(addr))

WebSocket: 30 / connection

The server caps each WebSocket connection at 30 simultaneous subscriptions. If you need more, shard them across multiple Info instances — each one lazy-builds its own WebsocketManager, and each manager owns one connection.

from dango.info import Info
from dango.utils.types import PairId
 
URL = "https://api-mainnet.dango.zone"
pairs = [PairId(f"perp/{coin}usd") for coin in ("eth", "btc", "sol", ...)]
 
shards = [Info(URL) for _ in range((len(pairs) + 29) // 30)]
for idx, pair in enumerate(pairs):
    shards[idx // 30].subscribe_perps_trades(pair, on_trade)

Remember to call info.disconnect_websocket() on each shard at shutdown.

Detecting a 429

Rate-limit responses arrive as HTTP 429. The SDK wraps any 4xx in ClientError with the response body truncated to 500 chars. Parse the message:

from dango.utils.error import ClientError
 
try:
    info.query_status()
except ClientError as exc:
    if "429" in str(exc):
        # back off
        ...

Detecting a subscription rate-limit error

Subscription rate-limit violations surface through the callback, not as an exception. Watch for the _error envelope:

def on_event(event):
    if isinstance(event, dict) and "_error" in event:
        # server dropped the subscription; reconnect or shard
        ...

Once a subscription receives an error, the manager has already removed its callback. Re-subscribe with a fresh id (and consider whether you have crossed the 30-per-connection cap).

Next