> For the complete documentation index, see [llms.txt](https://docs.nexus.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nexus.xyz/api-reference/guides/rate-limits.md).

# 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 more. A client that paces itself by counting requests gets refused while its own counter still looks healthy. This is the most common integration surprise with this API.

This page explains the model: what things cost, which budgets are separate, and how to read the headers. The **normative** definition is the OpenAPI contract, published at [`nexus-xyz/nexus-exchange-api`](https://github.com/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, so there is no multi-second reserve to bank by idling. And `remaining` can never exceed `limit`.

Most requests cost **1**. The exceptions:

| Cost                              | Applies to                                                                                                                                                           |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **5**                             | `GET /account/activity`, `GET /account/portfolio-history`, `GET /account/summary`, `GET /fills`, `GET /orders/history`, `GET /positions/pnl`, `GET /stats/referrals` |
| **`1 + floor(order_count / 40)`** | `POST /orders/batch`                                                                                                                                                 |
| **1**                             | every other operation the contract documents                                                                                                                         |

The heavy reads each fold or scan a large per-account buffer (fills, order history, the portfolio time-series, the merged activity feed), fan out across open positions, or aggregate venue-wide referral counts from the accounts backend. A batch submit's cost grows with its size, so up to 40 orders cost the same as one.

So a Pro caller at `limit: 20` gets 20 ticker reads per second, **or 4 `/fills` reads**, from the same budget on the same tier. Pace on the weight, not on the request count.

Two safety properties 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 matters. The contract lists the supported operations, and the rule holds across them, but it says nothing about other paths that happen 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, not against paths found by probing. Those operations are what 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:

| Class                       | Covers                                                                         | Charged against                                                          |
| --------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| **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](#websocket-ceilings)         |

An order write is charged to the trading bucket **instead of** the request bucket, not in addition to it. Because of the split, a burst of polling cannot starve your order placement, and order flow cannot starve your reads. It follows that **a healthy `x-ratelimit-remaining` on your last read tells you nothing about your order-placement headroom.** They are different pools, so read the pool you are about to spend from. `GET /account/rate-limit` reports each budget separately in `buckets`, and `buckets.order` answers whether you can place an order.

#### 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 endpoints (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. Submission can wait, and risk reduction cannot. A limiter that refuses your cancel while a position runs against you turns a fairness control into a loss. So the rule is unconditional. Exhausting submission can never refuse a cancel, because the two never draw on the same token.

For a client, this means:

* **Never infer your cancel headroom from a submission `429`.** An empty `order` bucket says nothing about the `cancel` one. A client that backs off all order calls 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 get a `429`. It carries `bucket: cancel`, the only refusal that means your cancel channel is saturated. Honour `retry-after` on that one.

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 applies only to the method that always reduces risk. If you need the guarantee, cancel.

`POST /orders/preview` is a trading action too, which is easy to miss. It is a write under `/orders` and costs a trading-class unit, the same as placing an order. 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, and 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, so pacing yourself cannot throttle you.

If you have just been promoted, note that a key's own ceiling is recorded when the key is created (the default is under [Tiers and current ceilings](#tiers-and-current-ceilings)), 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, and mint a fresh key if the reported minimum is not the one you expect.

### Reading the headers

* On **every** authenticated response: `x-ratelimit-limit` and `x-ratelimit-remaining`.
* On a **`429` only**, also: `x-ratelimit-reset` (unix seconds) and `retry-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` use different units, on purpose.** `remaining` counts unit-cost requests, so `x-ratelimit-remaining: 10` means ten weight-1 requests *or* two heavy ones. `retry-after` is derived from the weighted cost of the request that was refused. A limiter that reads `remaining` as "requests of the kind I am about to send" will over-send on heavy endpoints and trigger its own 429s.

A refusal is `HTTP 429` with this body:

```json
{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Order placement rate limit exceeded",
  "bucket": "order",
  "tier": "Pro"
}
```

Branch on `code`, then on **`bucket`**. It is one of `key`, `owner`, `order`, `cancel`, `ip` or `login`, and for the first four it is the key to look up in `GET /account/rate-limit`'s `buckets`. `ip` and `login` never appear there. Both meter a connection rather than an account, and `login` is refused before an account is even established. The same value is sent in the `x-ratelimit-bucket` header, so a proxy or retry wrapper can read it without parsing the body. The `message` names which pool ran out (`Rate limit exceeded`, `API key rate limit exceeded`, `Order placement rate limit exceeded`, or `IP rate limit exceeded`). It is a **diagnostic**. Its wording is not stable, so do not match it 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. The top-level fields cover the request class, and `buckets` covers every budget:

```json
{
  "tier": "pro",
  "limit": 20,
  "remaining": 17,
  "reset_at_ms": 1750000000123,
  "buckets": {
    "key":    { "limit": 20, "remaining": 17, "reset_at_ms": 1750000000123 },
    "owner":  { "limit": 20, "remaining": 19, "reset_at_ms": 1750000000051 },
    "order":  { "limit": 20, "remaining": 20, "reset_at_ms": 0 },
    "cancel": { "limit": 20, "remaining": 20, "reset_at_ms": 0 }
  }
}
```

**`buckets` is keyed by the same labels a `429` uses**, in both its `bucket` field and the `x-ratelimit-bucket` header. So a refusal maps straight onto the state that caused it: `buckets[error.bucket]`. Build your limiter around that lookup. `key` appears only when a per-key bucket gates you, and neither `ip` nor `login` ever appears, because both meter a connection rather than an account.

The top-level `limit`, `remaining` and `reset_at_ms` are the **request** class, unchanged, so nothing that reads them needs to change. They do not answer "may I place an order". `buckets.order` does. On a key that has just quoted its way to the submission ceiling the two disagree completely: `remaining: 20` alongside `buckets.order.remaining: 0`.

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. `buckets` comes back as `{}`, because no account-scoped budget meters that caller at all.

### Tiers and current ceilings

Tiers are multipliers on one model, not different models. **Pro** is the default for every account. An admin assigns **MarketMaker**. Request it through your Nexus contact, and see the [Market Maker Guide](https://docs.nexus.xyz/exchange/apis-and-rates/market-maker-guide).

| Tier          | Requests | Trading actions | WS connections | WS subscriptions | WS inbound frames |
| ------------- | -------- | --------------- | -------------- | ---------------- | ----------------- |
| `Pro`         | 20/s     | 20/s            | 5              | 50               | 10/s              |
| `MarketMaker` | 2,000/s  | 2,000/s         | 100            | 1,000            | 50/s              |

| Per client IP, on every tier              | Ceiling |
| ----------------------------------------- | ------- |
| Unauthenticated market-data reads         | 50/s    |
| `POST /auth/login`, in a pool of its own  | 5/s     |
| WebSocket connections, checked at upgrade | 5       |

A new API key is created with its own ceiling of **20/s**.

The per-IP ceilings apply regardless of tier, all of them before you hold a credential. `POST /auth/login` is metered far more tightly than reads, in a pool of its own. A login attempt costs an ecrecover over an EIP-191 signature, and anyone who can reach the endpoint can make the server pay that cost. The limit is not a guess-the-secret defence, since the signed message is a fixed constant and there is nothing to brute-force. It is a cost defence. Traffic whose client IP cannot be resolved at all is not admitted unthrottled either. Reads share one bucket and logins share a **second, separate** one, so a flood that strips its `x-forwarded-for` cannot drain the pool the rest of the unresolvable traffic depends on. An unresolvable origin gets a shared ceiling rather than no ceiling. Its size is set per deployment, so it is not listed above.

A login refusal carries `bucket: login`, which is the only refusal that means the login gate itself is saturated. It is a per-IP ceiling, so you share it with everyone behind your NAT or egress proxy, and it says nothing about your account. There is no account yet.

Cancellations get their own budget at the Trading-actions rate. The table gives them no column of their own because the two numbers are equal by construction.

**These numbers are current defaults, not a frozen contract.** They are rendered from the contract's `x-nexus-rate-limits` extension, which a test holds equal to what the service enforces, 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. Exhausting one does not affect the others.

**Connections** are capped per account by tier (the WS connections column above). A separate **per-IP cap** (the per-IP table) applies first, at upgrade time, and binds on every tier, 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. The single-use stream token is spent either way, so 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, because 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, the server **drops** over-limit frames and sends one `error` notice per accounting window rather than one per frame, so an inbound flood does not turn into an outbound one. Sustained flooding, meaning enough dropped frames inside one window, closes the connection with code **`1008`** (policy violation). The server does not apply a dropped frame and does not tell you. 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](/api-reference/guides/networks.md).

### Where enforcement lives today

One current limitation is visible from outside, and it works in your favour rather than against you.

The gateway process holds limiter state **in memory**, 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 comes from 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.

### Can your signer keep up?

The client signs every authenticated request before it leaves, so the signer sets a ceiling of its own under the budgets above. Check that ceiling before choosing an SDK. It depends far more on the scheme and the crypto library than on the language.

The Exchange accepts two request-signing schemes (see [Authentication](/api-reference/guides/authentication.md#signing-a-request-with-the-key) and [Agents](/api-reference/guides/agent-keys.md)):

* **HMAC** (`hmacAuth`). HMAC-SHA256 over the five-field canonical string, keyed with the hex-decoded API secret. A symmetric MAC takes microseconds in any language.
* **Agent key** (`agentAuth`). secp256k1 ECDSA over `keccak256` of the six-field canonical string, low-S, sent as 65-byte `r||s||v`. It is an elliptic-curve signature, so its cost depends on whether the library underneath is native code or pure interpreted code.

Measured through each SDK's own signing path, on one fixed `POST /api/v1/orders` body, on one thread. That is the same call the client makes on every request, including building the canonical string, hashing the body and hex-encoding the result, but no network I/O:

| SDK                              | Scheme    | Crypto underneath              |    p50 |    p95 | Signatures/s | p50 across 5 runs |
| -------------------------------- | --------- | ------------------------------ | -----: | -----: | -----------: | ----------------- |
| Rust (`nexus-exchange-rs`)       | HMAC      | `hmac` + `sha2`                | 0.9 µs | 0.9 µs |  \~1,080,000 | 0.88–0.96 µs      |
| Rust (`nexus-exchange-rs`)       | Agent key | `k256`                         |  79 µs |  91 µs |     \~12,500 | 74–79 µs          |
| TypeScript (`nexus-exchange-ts`) | HMAC      | Web Crypto (async)             |  27 µs |  43 µs |     \~29,000 | 24–35 µs          |
| TypeScript (`nexus-exchange-ts`) | Agent key | `@noble/curves`                | 319 µs | 470 µs |      \~2,900 | 293–645 µs        |
| Python (`nexus-exchange-py`)     | HMAC      | stdlib `hmac`                  | 2.1 µs | 2.2 µs |    \~460,000 | 1.96–2.25 µs      |
| Python, default install          | Agent key | `eth-keys` pure-Python backend | 3.5 ms | 4.2 ms |        \~280 | 3.43–4.11 ms      |
| Python + `coincurve`             | Agent key | libsecp256k1 via `coincurve`   |  97 µs | 110 µs |     \~10,000 | 96–116 µs         |

The CLI signs through the Rust crate, so it inherits the Rust rows rather than having numbers of its own.

**At the `Pro` tier, no SDK and no scheme is signer-bound.** 20 requests per second leaves 50 ms per signature. The slowest row, agent-key signing on a default Python install, uses 3.5 ms of it, so it can sign about fourteen times the Pro rate on one core. Even a caller saturating all three Pro buckets at once (requests, trading actions and cancels, 60 signed requests per second) spends about a fifth of one core signing. TypeScript's agent-key signer has more than a hundred times the headroom, and HMAC is effectively free in every SDK.

**At the `MarketMaker` tier, the scheme and the library start to matter.** Its 2,000 trading actions per second, plus a separate 2,000 cancels, leave 0.25–0.5 ms per signature on one thread.

* **Python with an agent key:** a default install signs about 280 per second, roughly a seventh of the trading ceiling. Install `coincurve` (`pip install coincurve`) in the same environment and `AgentSigner` runs on libsecp256k1 instead, at about 10,000 per second, with no change to your code. The SDK does not choose a curve backend itself; `eth-keys` uses `coincurve` whenever it can import it, unless the `ECC_BACKEND_CLASS` environment variable names a different backend. The Python bench prints the backend it measured, which is the quickest way to confirm the switch worked. Or sign with HMAC, which is never the constraint.
* **TypeScript with an agent key:** about 2,900 per second clears the trading ceiling on one core, but not trading and cancels together. A client that runs both at full rate should sign with HMAC or spread signing across worker threads.
* **Rust, and the CLI:** comfortable on either scheme.

Per request, signing adds 0.3 ms in TypeScript and 3.5 ms on a default Python agent-key install. That is a small fraction of a client round trip measured in tens of milliseconds, but noticeable on the Python path. That is another reason to install `coincurve` if you trade from Python with an agent key.

#### How these compare with Paradex's published figures

Paradex publishes a similar table: about 5,000 signatures per second in Rust, 1,430 in Go, 50 in TypeScript and 8 in Python or Java. Read at face value, TypeScript's 20 ms per signature caps one thread at 50 signatures per second. That covers a single 20/s budget, but signing alone would use 40% of the core, and it falls short of the 60/s a Pro caller can spend across requests, trading actions and cancels together. Those figures do not carry over. Paradex signs orders with a StarkNet key, a different curve and a different hash from either scheme here, and its numbers were not measured on the hardware below, so a ratio between the two tables is directional at best. The pattern does carry over. An elliptic-curve signer in pure interpreted code is the slow case, and native bindings close most of the gap. The difference here is that the slow case (Python on its pure-Python fallback, about 280 per second) is still more than ten times faster than the Pro tier needs.

#### Methodology

* **Hardware.** Apple M2, 8 cores (4 performance, 4 efficiency), 8 GB, macOS 26.5 (build 25F84), on AC power with Low Power Mode off. macOS cannot pin a process to a core, and the machine was not otherwise idle (load average 4–14 during the runs), so read the p95 column and the spread as upper bounds.
* **Runtimes.** Rust 1.97 (release profile), Node.js 22.23, Python 3.14 for the HMAC and default agent-key rows. The `coincurve` row runs on Python 3.13, because `coincurve` publishes no Python 3.14 wheel yet. The default agent-key row measures the same on 3.13 (3.5 ms), so the interpreter does not account for the difference.
* **Fixture.** Identical in all three SDKs: `POST`, path `/api/v1/orders`, empty query, a 155-byte limit-order body, a fixed timestamp. The agent signer issues a fresh nonce on every iteration, as it does on a real write. Both schemes produce byte-identical signatures across the SDKs for this fixture.
* **Procedure.** Two seconds of warm-up, then each signature is timed individually (Rust 20,000, TypeScript 5,000, Python 3,000 or 5,000) and p50 and p95 taken from the samples. Signatures per second is the sample count over the wall time of the timed loop. Each benchmark ran five times, interleaved with the others. The table shows the median run and the range of p50 across all five. The Rust bench also runs criterion, whose mean estimate agreed with the timed loop (76–85 µs for the agent key).
* **Rerun it.** Each SDK carries its bench: `cargo bench --bench signing` in `nexus-exchange-rs`, `pnpm bench` in `nexus-exchange-ts`, and `python bench/signing_bench.py` in `nexus-exchange-py`. The Python bench prints which `eth-keys` backend it measured.

### Designing for the limits

* **Batch instead of looping.** `POST /orders/batch` charges `1 + 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/summary` and `/account/portfolio-history` cost 5 each. Polling all four every second costs 20/s, which is a Pro caller's entire budget.
* **Prefer one coherent read.** `GET /account/state` returns the summary and every open position together. It is cheaper than two calls, and free of the race between them (see [Portfolio & Account State](/api-reference/guides/portfolio.md)).
* **Cache what does not change.** Market metadata from `GET /markets` does not need re-fetching every cycle.
* **Pace from the headers, and from `/account/rate-limit`.** Polling that endpoint is free. Retrying blindly after a `429` is not.

### Related

* [OpenAPI specification](https://github.com/nexus-xyz/nexus-exchange-api): normative per-operation weights, classes, and header semantics
* [Networks](/api-reference/guides/networks.md): how a network is selected, and what binds a key to one
* [Portfolio & Account State](/api-reference/guides/portfolio.md): the heavy-read endpoints, and how to read them in one call
* [Rate & Connection Limits](https://docs.nexus.xyz/exchange/apis-and-rates/rate-limits): HMAC window, token lifetimes, faucet allowance
* [Market Maker Guide](https://docs.nexus.xyz/exchange/apis-and-rates/market-maker-guide): requesting the MarketMaker tier
* [SDKs & Tools](/api-reference/guides/sdks.md)

> **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-limit` in 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.nexus.xyz/api-reference/guides/rate-limits.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
