Rate Limiting
This guide explains how rate limits work on the Synthetix off-chain trading APIs and how to build clients that stay within limits.
Overview
Rate limits protect the system and ensure fair access for all users. Limits apply per subaccount and per IP address.
| API | Per-subaccount limit | Per-IP limit |
|---|---|---|
REST (POST /v1/info, POST /v1/trade, GET /v1/exchange/status) | Yes (trade actions only) | Yes (all actions) |
| WebSocket | Yes (trade actions only) | Yes (all actions) |
How Limits Work
Limits use a token bucket model:
- Each subaccount and IP has a bucket of tokens
- Each request consumes tokens based on the action
- Tokens refill over time
- If a request would exceed available tokens, it is rejected with
429 Too Many Requests
Rate limit buckets (Mainnet)
Token costs are the same across environments. Bucket sizes may differ on Testnet. See Environments for environment-specific configuration.
| Limit type | Tokens per window | Window | Scope |
|---|---|---|---|
| Per-subaccount | Fee-tier dependent | 10 seconds | Trade actions only |
| Per-IP | 10,000 | 10 seconds | All actions |
Fee-tier rate limit budgets
REST and WebSocket trade actions use the subaccount's fee tier to determine the per-subaccount token budget. The default Mainnet budgets are:
| Tier ID | Tokens per 10 seconds |
|---|---|
tier_0 | 1,000 |
tier_1 | 1,200 |
tier_2 | 1,400 |
tier_3 | 1,600 |
tier_4 | 1,800 |
tier_5 | 2,000 |
tier_6 | 2,200 |
tier_7 | 2,500 |
top_tier | 2,500 |
market_maker | 5,000 |
Token costs — Trade actions (REST and WebSocket subaccount)
These costs apply to both REST POST /v1/trade and WebSocket trade actions. For placeOrders, cost = base cost × number of orders in the batch.
| Action | Base cost | Notes |
|---|---|---|
addDelegatedSigner | 100 | |
cancelAllOrders | 2 | |
cancelOrders | 2 | |
createSubaccount | 100 | |
getBalanceUpdates | 100 | |
getDelegatedSigners | 20 | |
getDelegationsForDelegate | 20 | |
getFeeRate | 10 | |
getFundingPayments | 100 | |
getOpenOrders | 10 | |
getOrderHistory | 50 | |
getPerformanceHistory | 100 | |
getPortfolio | 10 | |
getPositions | 10 | |
getRateLimits | 20 | |
getReferral | 20 | |
getSubAccount | 20 | |
getSubAccounts | 20 | |
getTrades | 20 | |
getTransfers | 10 | |
getWithdrawableAmounts | 100 | |
modifyOrder | 5 | |
modifyOrderBatch | 5 | |
placeIsolatedOrder | 5 | |
placeOrders | 5 | Cost = 5 × number of orders in batch |
removeAllDelegatedSigners | 100 | |
removeDelegatedSigner | 100 | |
scheduleCancel | 5 | |
transferCollateral | 5 | |
updateIsolatedMargin | 5 | |
updateLeverage | 5 | |
updateSubAccountName | 100 | |
voluntaryCollateralExchange | 100 | |
withdrawCollateral | 5 |
Token costs — info and status actions (per-IP limit only)
These costs apply only to the per-IP bucket. Info and status actions do not consume from the per-subaccount bucket.
| Action | Cost |
|---|---|
getCandles | 200 |
getCollaterals | 50 |
getExchangeStatus | 1 |
getFundingRate | 250 |
getFundingRateHistory | 1,000 |
getIsWhitelisted | 250 |
getLastTrades | 200 |
getMarketPrices | 200 |
getMarkets | 50 |
getMids | 50 |
getOpenInterest | 50 |
getOrderbook | 200 |
getSubAccountIds | 250 |
Trade actions also consume from the per-IP bucket using the same costs as the trade table above.
REST API
- Endpoints:
POST /v1/info,POST /v1/trade,GET /v1/exchange/status - Per-IP limit: Applies to all actions
- Per-subaccount limit: Applies to trade actions only
WebSocket API
This includes WebSocket messages and the GET /v1/ws/exchange/status status route.
Trade actions must pass two limits:
- Per-IP limit — Applies to all actions. Checked first.
- Per-subaccount limit — Applies to trade actions only.
Info and status actions only consume from the per-IP bucket. Multiple WebSocket connections from the same IP share the same per-IP budget.
Rate Limit Response
When you exceed a limit, the API returns:
- HTTP status:
429 Too Many Requests - Error code:
RATE_LIMIT_EXCEEDED - Category:
RATE_LIMIT - Retryable:
true
Rejected trade requests still include the top-level rateLimit snapshot when order rate limiting is enabled. See Rate Limit Snapshot on Trade Responses.
REST example
{
"success": false,
"clientRequestId": "abc-123",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "RATE_LIMIT",
"message": "Rate limit exceeded for action 'placeOrders'",
"retryable": true
}
}WebSocket example (subaccount limit)
{
"success": false,
"clientRequestId": "abc-123",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "RATE_LIMIT",
"message": "Rate limit exceeded for action 'placeOrders'"
}
}WebSocket example (IP limit)
{
"success": false,
"clientRequestId": "abc-123",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "RATE_LIMIT",
"message": "IP rate limit exceeded"
}
}Rate Limit Snapshot on Trade Responses
Every REST trade response (POST /v1/trade) and WebSocket trade response (via /v1/ws/trade) may include a top-level rateLimit object alongside the normal response envelope. This applies to all trade actions — including rejections due to rate limiting — when subaccount order rate limiting is enabled.
| Field | Type | Description |
|---|---|---|
requestsCap | integer | Maximum requests allowed in the current subaccount rate limit window |
remainingTokens | integer | Available tokens after debiting this request |
When order rate limiting is disabled or unavailable, rateLimit is omitted from the response envelope.
On HTTP 503 Request Timeout errors (REST or WebSocket), the rateLimit snapshot is attached when available, allowing integrators to inspect rate-limit state even when the request times out.
The getRateLimits action request and response schema are unchanged. Its requestsUsed and requestsCap fields report the same snapshot values as the envelope: requestsUsed = requestsCap - remainingTokens.
REST trade response example
{
"status": "ok",
"response": {
"statuses": [
{
"resting": {
"order": { "venueId": "1948058938469519360", "clientId": "cli-1948058938469519360" },
"id": "1948058938469519360"
}
}
]
},
"rateLimit": {
"requestsCap": 1200,
"remainingTokens": 1195
},
"request_id": "5ccf215d37e3ae6d",
"timestamp": "2026-06-23T12:00:00Z"
}WebSocket trade response example
{
"id": "order-1",
"requestId": "order-1",
"status": 200,
"timestamp": 1719144000000,
"rateLimit": {
"requestsCap": 1200,
"remainingTokens": 1195
},
"result": {
"status": "success",
"response": {
"statuses": []
}
}
}Implementation Notes for Bots
1. Use exponential backoff on 429
When you receive 429 with RATE_LIMIT_EXCEEDED, wait before retrying. Start with about 1 second and increase on repeated failures (e.g. 1s, 2s, 4s).
2. Pace placeOrders requests
Each order in a batch consumes 5 tokens (cost = 5 × batch size). A batch of 20 orders consumes 100 tokens. With a 1,000-token subaccount limit per 10 seconds, large batches can hit limits quickly. Consider:
- Smaller batches
- Spacing requests over time
- Avoiding bursts of many orders at once
3. Share IP budget across clients
Per-IP limits apply across requests from the same IP. If you run multiple bots or connections from one server, they share the same IP budget.
4. Treat rate limits as retryable
Rate limit errors are marked retryable: true. Retry after a short delay instead of treating them as permanent failures.
5. Avoid hammering after a 429
Back off and reduce request rate after hitting a limit. Continuing at the same rate will keep triggering 429s.
6. Read rateLimit from trade responses
Parse the top-level rateLimit object on every trade response to track remainingTokens without calling getRateLimits separately. The snapshot reflects post-debit state for the current request.
Related Documentation
- Environments - Environment-specific configuration
- Error Handling - Complete error handling guide
- General Information - API overview
- Place Orders - Order placement endpoint
- WebSocket API - WebSocket connection documentation