Rate Limits
What a request costs, what the rate-limit headers mean, which budgets are independent of which, and what happens when you exceed each.
The Exchange does not budget your traffic in requests per second. It budgets weight per second: every request is charged a cost, most requests cost one unit, and a few cost considerably more. A client that paces itself by counting requests will be refused while its own counter still looks healthy, which is the single most common integration surprise on this surface.
This page explains the model — what things cost, which budgets are separate, and how to read the headers. The normative definition lives in the OpenAPI contract, published at nexus-xyz/nexus-exchange-api and served at /openapi.json: the "Rate limits" section of the API description owns header semantics, and each operation carries its own machine-readable cost. Where this page and the contract disagree, the contract is right.
What a request costs
Your budget is a token bucket that refills continuously at your tier's per-second rate, with a capacity of exactly one second of tokens. Two consequences follow from that capacity: the sustained rate and the burst allowance are the same number — there is no multi-second reserve to bank by idling — and remaining can never exceed limit.
Most requests cost 1. The exceptions:
5
GET /account/summary, GET /fills, GET /orders/history, GET /account/portfolio-history
Each folds or scans a large per-account buffer — fills, order history, or the portfolio time-series
1 + floor(order_count / 40)
POST /orders/batch
Up to 40 orders cost the same as one; every further 40 adds a unit
1
every other operation the contract documents
The default
So a Pro caller at limit: 20 gets 20 ticker reads per second — or 4 /fills reads. Same budget, same tier, different arithmetic. Pace on the weight, not on the request count.
Two safety properties are worth knowing, because they bound the worst case. A single request's charge is capped at one second of tokens, so an oversized batch can never be permanently unsatisfiable — it drains the whole second once the bucket has refilled and goes through, rather than looping forever on a retry it could never afford. And a batch body the server cannot parse is charged the base unit rather than refused over weighting.
Rather than hardcoding the table above, read the cost from the contract: operations costing more than one unit carry x-nexus-rate-limit-weight, and those whose cost depends on the body also carry x-nexus-rate-limit-weight-formula. For an operation the contract documents, absence of the marker means weight 1. That is the form to build a client-side limiter against.
The qualifier is deliberate. The contract enumerates the supported surface, and that rule holds across it — but it is not a statement about any path that happens to answer. A route the contract does not list carries no marker, is not covered by the rule, and may be charged differently from what its absence suggests. Build against the operations the contract documents rather than against paths found by probing; that is the surface the weights, and this page, describe.
Four budgets, not one
There are four independent resource classes. Spending one does not spend the others, and each refuses in its own way:
Requests
Every REST operation that is not an order write
Your per-key and per-owner request buckets
Trading actions
POST and PATCH under /orders — marked x-nexus-rate-limit-class: trading
A dedicated order bucket, the same per-second size as the request bucket
Cancellations
DELETE under /orders — also marked x-nexus-rate-limit-class: trading
A separate cancel bucket, again the same per-second size
WebSocket control plane
Connections, subscriptions, and inbound client frames
Per-tier ceilings — see WebSocket ceilings
An order write is charged to the trading bucket instead of the request bucket, not in addition to it. That is the point of the split: a burst of polling cannot starve your order placement, and order flow cannot starve your reads. The corollary is the part to internalize — a healthy x-ratelimit-remaining on your last read tells you nothing about your order-placement headroom. They are different pools, and GET /account/rate-limit reports the request class only.
Cancellations never share a token with submission
Cancelling is the one action split out of trading into a budget of its own. A DELETE on the order surface — cancel-one, cancel-all, or cancel-by-market — is charged to the cancel bucket instead of the order bucket, so a key that has spent its entire submission allowance placing orders still has a full, untouched allowance for pulling them back.
This is the only deliberate asymmetry in the model, and the reason for it is worth stating plainly: submission can wait, risk reduction cannot. A limiter that refuses your cancel while a position runs against you has stopped being a fairness control and become a loss. So the rule is unconditional — exhausting submission can never refuse a cancel, because the two never draw on the same token.
Two things follow for a client.
Never infer your cancel headroom from a submission
429. Theorderbucket being empty says nothing at all about thecancelone. A client that backs its whole order surface off after one placement refusal has throttled the one call it should still be making.A separate bucket is not a bypass. Cancels are metered, at the same per-second tier rate as everything else, so a cancel loop can still
429— carryingbucket: cancel, which is the only refusal that actually means your cancel channel is saturated. Honourretry-afteron that one; it is real.
Amends are charged as submission, not cancellation. PATCH does not say whether an amend reduces or increases exposure, and a size-increasing amend is a submission by any reading — so the guarantee above is attached to the method that unambiguously reduces risk. If you need it, cancel.
POST /orders/preview is a trading action too, which catches people out: it is a write on the /orders surface and costs a trading-class unit exactly as placing an order does. Previewing before every order therefore halves your effective placement rate. Budget two trading-class charges per order placed that way, or skip the preview once you already know the sizing.
A caller presenting an HMAC key passes a per-key bucket and then the per-owner bucket for its tier; the effective ceiling is whichever binds first. GET /account/rate-limit reports that minimum, and polling it is free — it is the one operation that consumes no tokens, precisely so that pacing yourself cannot throttle you.
One wrinkle for anyone who has just been promoted: a key's own ceiling is recorded when the key is created (20/s by default) and a tier change does not rewrite it. A Market Maker account still using a key minted at the default can therefore be held at the key's number rather than the tier's. Read /account/rate-limit after a promotion instead of assuming the tier figure; mint a fresh key if the minimum reported is not the one you expect.
Reading the headers
On every authenticated response:
x-ratelimit-limitandx-ratelimit-remaining.On a
429only, additionally:x-ratelimit-reset(unix seconds) andretry-after(seconds, never below 1).
Do not expect the latter two on a success — a client that reads x-ratelimit-reset off a 2xx reads nothing.
remaining and retry-after are deliberately in different units. remaining is expressed in unit-cost requests: x-ratelimit-remaining: 10 means ten weight-1 requests or two heavy ones. retry-after, by contrast, is derived from the weighted cost of the request that was actually refused. A limiter that reads remaining as "requests of the kind I am about to send" will over-send on heavy endpoints and 429 itself.
A refusal is HTTP 429 with this body:
Branch on code. The message names which pool bottlenecked — Rate limit exceeded, API key rate limit exceeded, Order placement rate limit exceeded, or IP rate limit exceeded — and is a diagnostic: its wording is not stable and must not be matched programmatically.
Unlike the 403 jurisdiction refusals, a 429 is retryable: honour retry-after and back off. Pacing off x-ratelimit-remaining beats discovering the ceiling by hitting it.
The free GET /account/rate-limit reports the same state without spending a token, for the request class:
Two details to handle when you use both this endpoint and the headers. reset_at_ms is milliseconds, while the x-ratelimit-reset header is unix seconds. And the tier name is lower-cased here (pro) but not in the 429 body (Pro), so compare it case-insensitively rather than against a literal.
All three numeric fields are null for an Unlimited caller, which is bucketed per IP rather than per account.
Tiers and current ceilings
Tiers are multipliers on one model, not different models. Pro is the default for every account. MarketMaker is admin-assigned — request it through your Nexus contact, see the Market Maker Guide. Unlimited exists for gateway keys that multiplex many users and is never assigned to a trading account.
Pro
20/s
20/s
5
50
10/s
MarketMaker
2,000/s
2,000/s
100
1,000
50/s
Unlimited
per-IP, 50/s
per-IP, 50/s — the same bucket as reads
exempt
exempt
exempt
Two limits apply regardless of tier. Unauthenticated market-data reads are bucketed per client IP at 50/s. And traffic whose client IP cannot be resolved at all is not admitted unthrottled — it shares one strict bucket of 5/s, so an unresolvable origin degrades to a low ceiling rather than to no ceiling.
Cancellations get their own budget at the Trading-actions rate — a further 20/s for Pro, 2,000/s for MarketMaker — rather than a column of their own above, because the two numbers are equal by construction.
Unlimited deserves one caveat, because its exemptions are narrower than the name suggests. Its order writes and its cancels are not exempt from rate limiting: they skip both the dedicated trading bucket and the cancel bucket and are charged to the same per-IP bucket as its reads, so neither the class independence above nor the cancel guarantee holds there — a gateway's order flow can be crowded out by its own polling, on the tier whose traffic mix is least predictable. And the WS ceilings it is exempt from are the per-account ones; the per-IP connection cap below still binds.
These numbers are current defaults, not a contract. They are deployment configuration — some of them still code constants — and they will change as the tier system is finalized. Read /account/rate-limit rather than hardcoding them.
WebSocket ceilings
The WebSocket ceilings are a resource class of their own: independent of the REST request budget and of the trading-action budget, and exhausting one does not affect the others.
Connections are capped per account by tier (Pro 5, MarketMaker 100). A separate per-IP cap — 5 by default — is applied first, at upgrade time, and binds on every tier including Unlimited — so one origin address cannot reach the per-account figure on its own. A connection refused there gets an HTTP 429 (ws_conn_limit_exceeded) on the upgrade request rather than a close frame, and the single-use stream token is spent either way: mint a fresh one before reconnecting.
Subscriptions are capped at the same number twice — per connection and across all of an account's connections. Opening more sockets therefore does not buy more subscriptions. Re-subscribing a (channel, market) key you already hold replaces it in place and is free. Exceeding the ceiling returns an error frame (subscription_limit_exceeded) rather than a status code; there are no HTTP responses once the socket is open.
Inbound frames — your subscribes, unsubscribes and pings — are limited to the sustained per-tier rate with a 2× burst tolerated above it, so a reconnect-and-resubscribe storm is not penalized. Beyond that, over-limit frames are dropped, and you get one error notice per accounting window rather than one per frame (an inbound flood is not amplified into an outbound one). Sustained flooding — enough dropped frames inside one window — closes the connection with code 1008 (policy violation). A dropped frame is silently not applied: if you do not see a subscribed ack, re-send the subscribe after backing off rather than assuming it took effect.
Budgets are per network
Each network is its own deployment, so each network has its own buckets. Spending on testnet does not reduce mainnet headroom when mainnet launches, and neither does the reverse. Credentials do not cross networks either — a key is bound to the network that minted it. See Networks.
Where enforcement lives today
One honest limitation, because it is visible from outside and it moves in your favour rather than against you.
Limiter state is held in memory at the gateway process, not in a shared store. Two things follow. First, it is not durable: a redeploy resets your buckets, and a tier promotion may briefly fall back to the base tier until it is re-applied. Second, when a network's gateway runs more than one replica, each holds its own buckets, so the aggregate ceiling a client observes can be higher than the published per-second figure, depending on how its connections land.
Do not design against that headroom. Treat the published number as the ceiling you are entitled to and pace to it: the extra is an artifact of where enforcement currently lives, it is not distributed evenly, and it goes away when counters move to a shared store. A client built to the published figure keeps working when that lands; one tuned to the observed aggregate will start seeing 429s.
Designing for the limits
Batch instead of looping.
POST /orders/batchcharges1 + floor(n / 40), so 40 orders in one request cost a fortieth of 40 single submits.Stream instead of polling. Order book and trade data over WebSocket costs nothing against your request budget, and arrives sooner: the subscribe frame is one inbound frame, and the stream that follows is free.
Budget heavy reads at their real cost.
/fills,/orders/history,/account/summaryand/account/portfolio-historycost 5 each. Polling all four every second costs 20/s — a Pro caller's entire budget.Prefer one coherent read.
GET /account/statereturns the summary and every open position together: cheaper than two calls, and free of the race between them (see Portfolio & Account State).Cache what does not change. Market metadata from
GET /marketsdoes not need re-fetching every cycle.Pace from the headers, and from
/account/rate-limit. Polling that endpoint is free. Retrying blindly after a429is not.
Related
OpenAPI specification — normative: per-operation weights, classes, and header semantics
Networks — how a network is selected, and what binds a key to one
Portfolio & Account State — the heavy-read surface, and how to read it in one call
Rate & Connection Limits — HMAC window, token lifetimes, faucet allowance
Market Maker Guide — requesting the MarketMaker tier
Status: development preview on testnet. Mainnet has not launched. Every ceiling on this page is current configuration rather than a frozen contract — read
/account/rate-limitin production instead of hardcoding a number, and pin to a released spec version so the per-operation weights you build against stay fixed. Limiter state is not yet durable across gateway restarts. Testnet credentials and balances have no real-world value.
Last updated

