- 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: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 12setLeverage/withdraw calls per minute from a single IP.
Batch order writes (
batchPlaceOrders, 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 at 1,000 rows for most lists — 1,500 for candles, 100 for the two leaderboards — so the worst-case add-on is bounded: a full 1,000-row page of fills costs 20 + 50 = 70, and a full 1,500-bar candles page costs 20 + 25 = 45. l2OrderBook works the same way on order-book depth, charging on the depth you request rather than the rows returned: 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 large page can briefly drive your bucket negative; requests arriving before it refills are rejected with 429 and a Retry-After hint, as above.
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.
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: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:
used— units consumed since your last reseed.cap— current effective cap (starting cap plus volume replenishment).nextAvailableMs—0when 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:
Only client→server
subscribe / unsubscribe messages (and malformed frames) count against the outbound-message rate. Well-formed post / get RPCs are exempt — they’re bounded by the per-subaccount trading pools and the in-flight post cap instead. 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-Afteron a429and 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
Ncosts onlyfloor(N/40)IP weight (free on the IP base, like single writes) — far cheaper thanNseparate calls — though it still charges per item against the trading pool. You also save the per-request network round trips. - Poll
GET /v1/rateLimitif you run an aggressive order/cancel loop, and slow down asusedapproachescap. - Request smaller pages on list endpoints — a large page adds per-item weight after the fact.