Polyester rate-limits trading, account, security, and money-movement operations with weighted pools. REST, ConnectRPC, and trade WebSocket aliases for the same action share one quota.
Pools refill continuously. Capacity returns gradually; it does not reset on a fixed minute or hour boundary.
| Concept | Meaning |
|---|---|
| Pool | Shared unit bucket. Same string as policy_class in rejection responses. |
| Sustained rate | Refill speed, for example 120 units/min. |
| Burst capacity | Max units a full pool can spend immediately. |
| Cost | Units charged for one request or admitted batch item. |
ACCOUNT_ADMIN refills at 120 units/min (2 units/sec) and stores at most 30 units. Most account-admin mutations cost 2 units, so a full pool allows 15 immediate mutations (30 รท 2). After that, one more 2-unit mutation every second; full again in 15 seconds.
Defaults below can be overridden per account or API key. GET /v1/rate-limits returns the active
public VIP0-VIP10 placement and cancellation catalog. Authenticated clients can call GET /v1/trading/rate-limits to resolve the effective limits for their root account or an
accessible Subaccount. API-key callers receive both the account-scoped and API-key-scoped rules
because both scopes are enforced.
These endpoints return policy configuration, not live remaining capacity. Response headers and rejection details remain authoritative for current usage and retry timing.
Limits by pool
Each pool lists its unit capacity and scopes, then the operations that spend from it. A blank Pool cell means the row shares the pool above. Sustained and burst already include cost.
Same pool = shared capacity. Extra API keys do not add capacity. Any exhausted scope rejects the request.
Authentication and account management
| Pool | Capacity | Scopes |
|---|---|---|
AUTH_PUBLIC | 30 units/min ยท burst 30 | CLIENT_IP, AUTH_SUBJECT |
ACCOUNT_ADMIN | 120 units/min ยท burst 30 | ACCOUNT, API_KEY, SUBACCOUNT |
ACCOUNT_SECURITY | 60 units/min ยท burst 20 | ACCOUNT, API_KEY, SUBACCOUNT |
SOCIAL_PROVIDER | 10 units/hour ยท burst 3 | ACCOUNT, API_KEY, SUBACCOUNT |
| Pool | Operation | Cost | Sustained | Burst |
|---|---|---|---|---|
AUTH_PUBLIC | Wallet nonce or login | 1 unit | 30 req/min | 30 req |
ACCOUNT_ADMIN | Profile, Subaccount, member, invite, or address-book mutation | 2 units | 60 req/min | 15 req |
ACCOUNT_SECURITY | MFA challenge or step-up | 1 unit | 60 req/min | 20 req |
| Access-policy, MFA enrollment, or MFA factor mutation | 2 units | 30 req/min | 10 req | |
| API key create, update, or delete; recovery-code regeneration | 5 units | 12 req/min | 4 req | |
SOCIAL_PROVIDER | Start external social verification | 1 unit | 10 req/hour | 3 req |
AUTH_PUBLIC is dual-scoped: client IP and wallet subject (AUTH_SUBJECT). Hitting either limit
rejects the request.
Deposits, withdrawals, and internal transfers
| Pool | Capacity | Scopes |
|---|---|---|
DEPOSIT_CREATE | 10 units/min ยท burst 3 | ACCOUNT, API_KEY, SUBACCOUNT |
INTERNAL_TRANSFER | 60 units/min ยท burst 10 | ACCOUNT, API_KEY, SUBACCOUNT |
WITHDRAW_SUBMIT | 10 units/min ยท burst 5 | ACCOUNT, API_KEY, SUBACCOUNT |
WITHDRAW_VALIDATE | 120 units/min ยท burst 30 | ACCOUNT, API_KEY, SUBACCOUNT |
| Pool | Operation | Cost | Sustained | Burst |
|---|---|---|---|---|
DEPOSIT_CREATE | Create a deposit address | 1 unit | 10 req/min | 3 req |
INTERNAL_TRANSFER | Create an internal transfer | 1 unit | 60 req/min | 10 req |
WITHDRAW_SUBMIT | Submit a trading withdrawal | 1 unit | 10 req/min | 5 req |
WITHDRAW_VALIDATE | Validate a withdrawal destination | 1 unit | 120 req/min | 30 req |
WITHDRAW_SUBMIT: 5 immediate when full, then one every six seconds. Long-term: 10/min.
Guard signer
| Pool | Capacity | Scopes |
|---|---|---|
GUARD_SIGN | 300 units/min ยท burst 100 | ACCOUNT, API_KEY, SUBACCOUNT |
| Pool | Operation | Cost | Sustained | Burst |
|---|---|---|---|---|
GUARD_SIGN | Create a wallet or sign a protected action | 5 units | 60 req/min | 20 req |
| Batch sign protected actions | 5 units/item | 60 items/min | 20 items | |
| Rotate or export a Guard signer wallet | 10 units | 30 req/min | 10 req |
Trading
Placement and cancellation are separate pools per trading account (root or Subaccount). All keys and transports for that account share those pools. Limits are account-wide across symbols: BTC and ETH orders draw from the same placement pool. Placement exhaustion does not reduce cancellation capacity. Replaying an already admitted trading request with the same idempotency key does not charge again.
Scopes for both pools: ACCOUNT, API_KEY, SUBACCOUNT. Batch rows charge per admitted item;
other rows charge per request. Place and cancel columns are TRADING_PLACE and TRADING_CANCEL weighted capacity from the public catalog.
| Operation | Cost |
|---|---|
| Place: create, modify, resume | 1 |
| Place: batch create/replace | 1/item |
Cancel: single, pause, CancelAllAfter | 1 |
| Cancel: batch | 1/item |
| Cancel all | 10 |
Sustained and burst for each operation are the VIP catalog quota divided by that cost. Higher professional or market-maker capacity requires an account-specific override and is not granted automatically by retail VIP status.
Read and realtime quotas are not published yet. Other ingress and abuse controls can still reject requests outside these pools.
Rejection contracts
policy_class in the error is the pool name (WITHDRAW_SUBMIT, ACCOUNT_SECURITY, โฆ).
Quota exceeded: HTTP 429 with application/problem+json.
| Header | Meaning |
|---|---|
RateLimit-Limit | Active weighted limit for the rejected pool |
RateLimit-Remaining | Remaining capacity at rejection |
RateLimit-Reset / Retry-After | Seconds until a retry may succeed, rounded up |
X-Polyester-RateLimit-Policy-Version | Policy version used |
Body includes millisecond precision:
{
"type": "https://polyester.dev/problems/rate-limit-exceeded",
"title": "Rate limit exceeded",
"status": 429,
"detail": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"rate_limit": {
"reason": "QUOTA_EXCEEDED",
"limit": "10",
"remaining": "0",
"retry_after_ms": "6000",
"policy_version": "3",
"operation_id": "withdraw.submit",
"policy_class": "WITHDRAW_SUBMIT",
"scope": "ACCOUNT",
"refill_model": "CONTINUOUS"
}
}uint64 fields are JSON strings. scope is which dimension rejected the call: CLIENT_IP, AUTH_SUBJECT, API_KEY, ACCOUNT, or SUBACCOUNT.
resource_exhausted, same rate-limit headers, plus polyester.ratelimit.v1.RateLimitDetail in
error details.
Protobuf: RATE_LIMIT_EXCEEDED with RateLimitDetail on the order error. SBE carries the same
fields in fixed-width form. A quota rejection does not close the connection.
Retry on 429
For 503 / RATE_LIMIT_AUTHORITY_UNAVAILABLE, see Authority failures.
Wait
retry_after_ms if present, else Retry-After. Add a little jitter.Keep the same request
CONFLICT_DUPLICATE_CLIENT_ORDER_ID instead of replaying the earlier result.Re-sign API keys
Stop eventually
See Client order IDs and idempotency.
Authority failures
503 with RATE_LIMIT_AUTHORITY_UNAVAILABLE (ConnectRPC: unavailable, reason AUTHORITY_UNAVAILABLE). That is not proof the mutation failed or succeeded. Keep the idempotency key, back off, and reconcile before sending a new logical request.