# Errors and rate limits

Read SBE WebSocket rejects, decide whether a command changed anything, and handle rate-limit, overload, and session limits.

A rejected command is answered with a `CommandReject` and the connection stays open. Only an answer without a definite outcome ends the connection, with close code `1013`: an outcome-unknown code, or `UNAVAILABLE` for a command that could not be sent. The reject's `code` tells you whether anything changed and what to do next.

## Read a CommandReject

| Field                  | Meaning                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `seq`, `correlationId` | Copied from the command; `0` when the frame could not be decoded                                            |
| `code`                 | A `RejectCode` value. Branch on this                                                                        |
| `tsNs`                 | Rejection time                                                                                              |
| `rateLimit*`           | Quota details for `RATE_LIMIT_EXCEEDED`; null or zero for rejects unrelated to rate limits                  |
| `existingOrderId`      | For `CONFLICT_DUPLICATE_CLIENT_ORDER_ID`: the order holding that client order ID, when known; `0` otherwise |
| `detail`               | UTF-8 text for logs and display. Do not parse it                                                            |

`RejectCode` uses the same numbers as the order error codes of the REST and ConnectRPC APIs, plus `SEQUENCE_OUT_OF_ORDER` (`1001`) for a sequence gap on the connection. The same numbers appear in batch item results and in `OrderEvent.rejectReason`. The full list is in the [Message reference](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/message-reference#rejectcode).

## Decide what to do

Most rejects change nothing. The groups below cover the codes an order-entry client meets most often; the message reference describes every code.

- **Encoding or structure** · state changed: no\
  `BAD_REQUEST` (1), `INVALID_ARGUMENT` (2), `BATCH_TOO_LARGE` (33), `VALIDATION_ERROR` (66)\
  Fix the client. A wrong schema version or block length is `BAD_REQUEST`.
- **Session sequence** · state changed: no\
  `SEQUENCE_OUT_OF_ORDER` (1001)\
  Resend with the expected `seq`; see [Sequence numbers](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/connection-and-authentication#sequence-numbers).
- **Order parameters** · state changed: no\
  `BAD_PRICE` (5), `BAD_QTY` (6), `MIN_NOTIONAL` (7), `QTY_STEP_SIZE` (8), `PRICE_TICK_SIZE` (30), `MIN_QTY` (31), `MAX_QTY` (74), `MAX_NOTIONAL` (75), `INVALID_TIME_IN_FORCE` (202), `FEE_ASSET_NOT_ALLOWED` (15), `MAX_SLIPPAGE_INVALID` (48), `MAX_QUOTE_DEBIT_TOO_SMALL` (68)\
  Align the value with the Pair's limits from [`GET /v1/spot/config`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/spot/config).
- **Funds and account limits** · state changed: no\
  `INSUFFICIENT_FUNDS` (3), `RISK_LIMIT` (55), `MAX_OPEN_ORDERS_TOTAL` (205), `MAX_OPEN_ORDERS_PER_PAIR` (306), `POLICY_MAX_OPEN_ORDERS` (34), `POLICY_MAX_NOTIONAL` (21)\
  Free funds or cancel orders first.
- **Market state** · state changed: no\
  `UNKNOWN_SYMBOL` (4), `PAIR_DISABLED` (16), `PAIR_NOT_LISTED_YET` (50), `PAIR_DELISTED` (51), `PAIR_CANCEL_ONLY` (52), `PAIR_POST_ONLY` (53), `PAIR_REDUCE_ONLY` (54), `MARKET_HALTED` (56), `ORDER_TYPE_NOT_ALLOWED_IN_AUCTION` (274), `POST_ONLY_NOT_ALLOWED_IN_AUCTION` (275)\
  Wait for the Pair's status to change.
- **Market orders** · state changed: no\
  `MARKET_PRICE_UNAVAILABLE` (37), `EMPTY_BOOK` (62), `STALE_QUOTE` (65), `MARKET_QUOTE_STALE` (200), `MARKET_QUOTE_UNHEALTHY` (201), `PRICE_BAND_VIOLATION` (60)\
  Retry later or send a limit order.
- **Permissions and policy** · state changed: no\
  `PERMISSION_DENIED` (12), `POLICY_MARKET_DENY` (20), `POLICY_TRADING_HALTED` (22), `POLICY_SPOT_TRADE_DENY` (23), `API_KEY_POLICY_REQUIRED` (27), `API_KEY_MARKET_DENY` (29), `ACCOUNT_ORDERS_SUSPENDED` (263)\
  Check the key's policy and the account's permissions; do not retry unchanged.
- **Order identity** · state changed: see the codes\
  `CONFLICT_DUPLICATE_CLIENT_ORDER_ID` (9), `CONFLICT_IDEMPOTENCY_KEY_REUSE` (36), `ORDER_UNKNOWN` (17), `ORDER_ALREADY_TERMINAL` (64), `MODIFICATION_REQUIRES_REPLACE` (35), `QTY_AT_OR_BELOW_FILLED` (76)\
  `QTY_AT_OR_BELOW_FILLED` ended the order at its filled quantity; the others changed nothing.
- **Busy, retry shortly** · state changed: no\
  `UNAVAILABLE` (10), `OVERLOADED` (67), `PAIR_BACKLOGGED` (207), `ORDER_MODIFY_IN_PROGRESS` (226), `ORDER_PLACEMENT_PENDING` (227), `SETTLEMENT_BACKLOGGED` (84), `ACCOUNT_REPAIRING` (300), `FEE_SCHEDULE_UNAVAILABLE` (220), `POLICY_UNAVAILABLE` (221)\
  Retry after a short backoff with jitter.
- **Rate limits** · state changed: no\
  `RATE_LIMIT_EXCEEDED` (69), `SYSTEM_RATE_LIMITED` (225), `RATE_LIMIT_POLICY_UNAVAILABLE` (224)\
  See [Rate limits](#rate-limits).
- **Outcome unknown** · state changed: maybe\
  `ORDER_CREATE_OUTCOME_UNKNOWN` (303), `ORDER_CANCEL_OUTCOME_UNKNOWN` (297), `ORDER_MODIFY_OUTCOME_UNKNOWN` (298)\
  Read the order before retrying; the connection closes with `1013`.
- **Server** · state changed: not known\
  `INTERNAL_ERROR` (18), `UPSTREAM_ERROR` (14), `GATEWAY_MISCONFIGURED` (77)\
  Reconcile the affected orders, then report it to support with the `seq`, `correlationId`, and `tsNs`; retrying `GATEWAY_MISCONFIGURED` does not help.

Some refusals are not command rejects. A post-only order that would cross, a fill-or-kill without enough liquidity, or a market order the matching engine cannot fill may be admitted and then end as `REJECTED` or `CANCELED` in its `OrderEvent`, with `rejectReason` or `cancelReason` set.

### Outcome unknown

An outcome-unknown code means Polyester sent the command onward but did not confirm the result in time. The command may still apply. The server writes the reject, then closes the connection with `1013` and does not process any command you sent after it on that connection.

Before resending anything:

1. Read the affected orders by `clientOrderId` or `orderId`, or read all open orders.
2. Resolve every command that was unanswered when the connection closed.
3. For a batch, resend the same `correlationId` and items: decided items return their original results and the rest are processed once.

`UNAVAILABLE` on its own means the command was not accepted. When an `UNAVAILABLE` reject is followed by a `1013` close, treat the commands after it as unanswered.

## Rate limits

SBE WebSocket commands spend from the same trading quotas as REST and ConnectRPC. Placement and cancellation are separate pools per trading account, shared by every key and connection of that account. A batch spends per admitted item. The pools, costs, and current limits are on [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits#trading). Read the limits that apply to your account with [`GET /v1/trading/rate-limits`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/trading/rate-limits) (ConnectRPC [`GetTradingRateLimits`](https://testnet.polyester.com/docs/api-docs/connect/ratelimit.v1.RateLimitService/GetTradingRateLimits)).

| Command                                   | Pool                       |
| ----------------------------------------- | -------------------------- |
| `CreateOrder`, `ModifyOrder`              | `TRADING_PLACE`            |
| `BatchCreateOrders`, `BatchReplaceOrders` | `TRADING_PLACE`, per item  |
| `CancelOrder`, `CancelAllOrders`          | `TRADING_CANCEL`           |
| `BatchCancelOrders`                       | `TRADING_CANCEL`, per item |
| `Ping`, `Subscribe`                       | None                       |

A rate-limit rejection carries the quota details and the minimum retry delay in its `rateLimit*` fields; for any other rejection they are null or zero.

| Field                    | Meaning                                                                           |
| ------------------------ | --------------------------------------------------------------------------------- |
| `rateLimitWeight`        | Maximum weighted capacity of the exhausted quota                                  |
| `rateLimitRemaining`     | Capacity left after the decision. Null (`UINT64_MAX`) when not rate-limit related |
| `rateLimitRetryAfterNs`  | Minimum delay in nanoseconds before retrying. `0` means no delay applies          |
| `rateLimitPolicyVersion` | Version of the quota policy used                                                  |
| `rateLimitOperation`     | The operation that was limited, such as `CREATE_ORDER` or `BATCH_CANCEL_ORDERS`   |
| `rateLimitPolicyClass`   | `TRADING_PLACE` or `TRADING_CANCEL`                                               |
| `rateLimitScope`         | `ACCOUNT`: the capacity belongs to the trading account                            |
| `rateLimitRefillModel`   | `CONTINUOUS`: capacity returns gradually, not at a fixed boundary                 |

In a batch, rate-limited items are rejected individually with `RATE_LIMIT_EXCEEDED`. The batch acknowledgement then carries `rateLimitWeight`, `rateLimitRemaining`, `rateLimitRetryAfterNs`, and `rateLimitPolicyVersion`; they are null or zero when no item was rate-limited.

To retry, wait at least `rateLimitRetryAfterNs` plus a little jitter, keep idempotency identities unchanged, and do not split a limited batch into many single commands.

### Limits that are not your quota

| Code                            | Meaning                                                                                                                | What to do                               |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `OVERLOADED`                    | Your account has too many order requests outstanding across all its connections, or the trading service is at capacity | Retry once earlier commands are answered |
| `SYSTEM_RATE_LIMITED`           | The venue-wide admission rate shared with other accounts is exhausted                                                  | Retry shortly with backoff               |
| `RATE_LIMIT_POLICY_UNAVAILABLE` | No usable rate-limit policy applies to the account right now                                                           | Retry later                              |
| `PAIR_BACKLOGGED`               | The Pair is busy with your account's earlier requests; nothing was reserved                                            | Retry shortly                            |

`OVERLOADED` carries no `rateLimit*` details: it is not a quota.

### Session and connection limits

Up to 16 concurrent sessions per API key and 64 per account are always accepted, and one IP address can hold 200 open connections. Upgrades above these are refused with HTTP `429`; see [Upgrade failures](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/connection-and-authentication#upgrade-failures). Commands on this interface do not count against the per-IP request limit of REST and ConnectRPC order writes.

### Why did my first command get SEQUENCE_OUT_OF_ORDER?

The first command of every connection must use `seq` `1`, including after a reconnect. A client that keeps its counter across connections is rejected until it restarts at `1`.

### Why is every command rejected with BAD_REQUEST?

The header's `version` is not the version your environment accepts, the `blockLength` does not match the schema, or the frame has trailing bytes. Download the environment's schema again and regenerate. The `detail` text names the cause, for example "unexpected SBE schema version".

### Why was my MARKET order rejected with BAD_REQUEST?

A `MARKET` order must use `IOC` and leave `priceTicks` null. An encoder that leaves `priceTicks` at `0` sends a price. See [Null values](https://testnet.polyester.com/docs/developer-docs/sbe-websocket/schemas-and-codecs#null-values).
