Skip to main content
Arcus throttles traffic in two independent layers. Both can reject the same request, and a request must pass both to succeed:
  • Per-IP weight limits — every REST request (and WebSocket message) costs a weight. Each source IP gets a token budget that refills over time; expensive endpoints drain it faster.
  • Per-subaccount trading limits — order placement and cancellation draw from two pools keyed to your subaccount, independent of IP. These scale with your realized trading volume.
Limits are enforced globally across all gateway nodes (state lives in a shared store), so spreading requests across connections or hitting different nodes does not raise your effective budget.

Per-IP weight limits

Every IP gets a token bucket holding 1,500 weight, refilling continuously at 1,500 weight per minute (25 weight/second). Each request deducts its weight before the handler runs. When the bucket can’t cover a request’s weight, the request is rejected.

Rejection contract

A throttled request returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{"error":"rate limited"}
Retry-After is the whole number of seconds to wait before the bucket holds enough tokens to retry (minimum 1). Arcus does not return X-RateLimit-* headers — the budget and per-endpoint weights are published here instead, so you can account for cost client-side rather than discovering it per response.

Weight tiers

Endpoints fall into a handful of tiers by how much work they cost the backend. With a 1,500/min budget that’s roughly 750 cheap reads, 75 standard list calls, or 12 cancelAllOrders per minute from a single IP.
WeightEndpoints
0health — never touches the limiter (use it for liveness probes). Single order writesplaceOrder, modifyOrder, cancelOrder — are also free on the IP layer; they’re governed entirely by the per-subaccount trading pools below.
1/ (root)
2Cheap single-object reads — bbo, mids, account, positions, order, feeTiers, leverages, accountStats, rateLimit
20Standard data, lists, and management — prices, markets, trade, trades, candles, portfolio, openOrders, orders, fills, funding, fundingRates, accountTransferUpdates, apiKeys, createApiKey, revokeApiKey, userPreferences, all affiliate endpoints
125Heavyweight writes — cancelAllOrders (fans out across your whole book), setLeverage, withdraw
Batch order writes (batchPlaceOrders, batchModifyOrders, batchCancelOrders) are also free on the IP base; they incur only a post-flight per-item charge of floor(N/40) weight for a batch of N — so a batch of up to 39 orders costs 0 IP weight. As with single writes, the real limit is the per-subaccount trading pool (each element charges the pool 1). List endpoints add a per-item charge on top of their base weight, computed from the rows actually returned: total = base + floor(items / N), where N is 20 for most lists (60 for candles). Page sizes are capped (2,000 rows for most lists, 1,000 for accountTransferUpdates), so the worst-case add-on is bounded — e.g. a full 2,000-row page of fills costs 20 + 100 = 120. l2OrderBook works the same way on order-book depth: 2 + floor(nLevels/20), maxing at weight 7 for a 100-level book. Because the row count isn’t known until the query runs, this charge is applied after the response is sent, so a single oversized page can briefly drive your bucket negative; your next request then waits for it to recover.

Per-subaccount trading limits

Order placement and cancellation draw from two pools scoped to your subaccount (address + account index), enforced on both the REST and WebSocket order paths. This is the address-based analogue to the per-IP layer: it bounds how fast a single account can act, regardless of how many IPs or connections it spreads across.
PoolStarting capCharged by
Order20,000placeOrder / modifyOrder — 1 per order (N for a batch of N)
Cancel40,000cancelOrder — 1 per cancel (N for a batch of N); cancelAllOrders — flat 1,000
The pools are independent: a cancel-heavy strategy can drain the cancel pool while the order pool sits nearly full, or vice versa.

Volume-based replenishment

Each pool’s effective cap grows with your lifetime realized notional — the more you trade, the more headroom you get:
effective cap = starting cap + lifetime_notional_usd / 0.10
Every $0.10 of realized fill notional adds 1 unit of pool headroom (so $1 of volume buys 10). Roughly $100K of cumulative volume buys +1,000,000 headroom on each pool, and high-volume market makers (≥ $1M lifetime) are effectively unconstrained. The counter is cumulative and never decays, so caps only ever rise.

When a pool is empty

Past the effective cap, a slow drip takes over: 1 action per 10 seconds per pool. This keeps a depleted account alive at a trickle rather than hard-failing, while still throttling abuse. Charges that exceed the remaining pool are rejected the same way as the IP layer (with a retry hint).
Charges are refunded automatically if an accepted order fails to publish downstream (a transient internal error). Engine rejections do not refund — the matching-engine work was already consumed.

Checking your remaining budget

GET /v1/rateLimit?address=<0x...>&account_index=<n> returns a live snapshot of both pools:
{
  "address": "0x...",
  "accountIndex": 0,
  "order":  { "used": 1200, "cap": 12000, "nextAvailableMs": 0 },
  "cancel": { "used": 0,    "cap": 22000, "nextAvailableMs": 0 }
}
  • used — units consumed since your last reseed.
  • cap — current effective cap (starting cap plus volume replenishment).
  • nextAvailableMs0 when you have headroom; otherwise the milliseconds until the next drip token frees up.

WebSocket limits

WebSocket connections are bounded per IP, in addition to the per-message weight charged against your IP bucket:
LimitValueScope
Simultaneous connections50per IP
New connections50 / minper IP
Subscriptions per connection100per socket
Subscriptions1,000per IP (across all connections)
Outbound messages1,000 / minper IP
In-flight post messages50per connection
Connection lifetime24 hoursper connection (auto-closed after)
Only client→server messages count against the outbound-message rate — subscribe, unsubscribe, and post / get RPCs. Server-pushed channel_data updates are free. A rejected new connection still consumes a new-connection token, so a client hammering the connection cap burns its own budget faster.

Handling rate limits

  • Read Retry-After on a 429 and back off for at least that long. A blind retry loop will keep losing.
  • Prefer WebSocket subscriptions over REST polling for anything that changes frequently (order book, fills, account state). One subscription replaces a stream of weighted REST calls.
  • Batch order operations. A batch of N costs only floor(N/40) IP weight (free on the IP base, like single writes) — far cheaper than N separate calls — though it still charges per item against the trading pool. You also save the per-request network round trips.
  • Poll GET /v1/rateLimit if you run an aggressive order/cancel loop, and slow down as used approaches cap.
  • Request smaller pages on list endpoints — a large page adds per-item weight after the fact.
If your use case legitimately needs higher limits (e.g. a high-volume market maker), reach out about IP allowlisting for your egress addresses.