---
title: "Handling Crypto Exchange API Rate Limits in JavaScript"
description: "Master handling exchange rate limits JavaScript and Node.js to prevent dropped orders. Build a robust throttling architecture using Siebly SDKs as your base."
canonical: "https://siebly.io/blog/handling-crypto-exchange-api-rate-limits-in-javascript-and-nodejs"
---

# Handling Crypto Exchange API Rate Limits in JavaScript and Node.js

Master handling exchange rate limits JavaScript and Node.js to prevent dropped orders. Build a robust throttling architecture using Siebly SDKs as your base.

## Technical Disclaimer

These articles are software engineering references for exchange API integrations. They are not financial, investment, legal, tax, compliance, or trading advice. Use public data, demo, testnet, or paper workflows first. Keep API credentials out of frontend code and disable withdrawal permissions for automation keys.

## Overview {#overview}

A single 429 Too Many Requests response in a volatile market is not a small glitch. It can drop orders and leave your local state out of sync with the exchange. Sleeping between calls is not enough. Production systems need to model how each venue actually counts traffic: request weight on Binance, per-endpoint request windows on Bybit and OKX, IP buckets on public Gate.io endpoints, and decaying call counters on Kraken.

This guide shows how to read those signals, queue work, and fall back to WebSockets when REST polling is too expensive. The examples use Siebly SDKs ([binance](/sdk/binance/javascript), [bybit-api](/sdk/bybit/javascript), [okx-api](/sdk/okx/javascript), [bitget-api](/sdk/bitget/javascript), [kucoin-api](/sdk/kucoin/javascript), [coinbase-api](/sdk/coinbase/javascript), [gateio-api](/sdk/gate/javascript), [bitmart-api](/sdk/bitmart/javascript), [@siebly/kraken-api](/sdk/kraken/javascript), [@siebly/htx-api](/sdk/htx/javascript)). Those clients sign requests, keep clocks in range, and give you typed methods. They do not throttle or queue for you. That is still your job.

## Key Takeaways {#key-takeaways}

- Treat IP limits and API-key / UID limits as separate buckets. Several Node processes on one VPS share the IP bucket.
- Parse the headers each venue actually sends. Binance uses `x-mbx-used-weight-1m`. Bybit uses `X-Bapi-Limit-Status` (remaining), `X-Bapi-Limit` (max), and `X-Bapi-Limit-Reset-Timestamp`.
- Do not assume every exchange uses Binance-style weights. Bybit V5, OKX, Gate.io, Bitget, KuCoin, BitMart, and HTX mostly count requests per endpoint and window. Kraken uses a decaying counter.
- Use a Token Bucket or Leaky Bucket in front of SDK calls. The SDKs will not pause traffic for you.
- Move market data off REST polling and onto WebSocket streams. Where the venue supports it, place orders over an awaitable WebSocket API so those calls stop eating REST quota.



## Understanding the Complexity of Crypto Exchange Rate Limits {#understanding-the-complexity-of-crypto-exchange-rate-limits}

Most first versions of a bot cap "requests per second" and call it done. That works until one heavy endpoint blows the budget. [Binance](/sdk/binance/javascript) assigns a weight to each route. A ticker is cheap. A deep order book or a large kline window is not. [Gate.io](/sdk/gate/javascript) public traffic is counted per IP and per endpoint. Private traffic is usually counted per UID. During stress, some venues also tighten order-entry limits based on fill ratio, not just raw request volume.

### Request Counts vs. Weighted Limits {#request-counts-vs-weighted-limits}

Official exchange docs are the source of truth for the numbers, and they change. On Binance, `GET /api/v3/ticker/price` is a small weight. `GET /api/v3/klines` grows with `limit` and can cost 10 or 20 weight. `GET /api/v3/exchangeInfo` has sat at 10 or 20 depending on the current spec, so do not hardcode it. If you only count HTTP calls, you will hit 429s while your request counter still looks fine.

Bybit V5 is a different model. Limits are per UID and per endpoint (for example 10 requests per second on some trade routes). The matching headers tell you remaining requests, not remaining weight. A Token Bucket still works, but each token should represent one request against that endpoint, not a Binance weight unit.

OKX, Bitget, KuCoin, Gate.io, BitMart, and HTX are closer to Bybit: fixed request windows per route. Kraken is different again. Private REST calls consume a counter that recovers over time, so a burst of cheap calls can still lock you out.

### IP and Account-Level Throttling Boundaries {#ip-and-account-level-throttling-boundaries}

IP limits sit in front of everything that hits the same address. Several workers on one VPS share that bucket. Account limits sit on the API key or UID and often scale with VIP tier.

On Binance, repeated 429s without backing off can escalate to HTTP 418, an automated IP ban that can last minutes to days. On Gate.io, public endpoints are IP-based (currently on the order of 200 requests per 10 seconds per endpoint). Over the burst threshold the request is declined. Separate fill-ratio rules can also cut your order-entry rate for at least an hour. That is not the same as a multi-hour IP ban, so do not treat every venue as Binance 418.

[bybit-api](/sdk/bybit/javascript) and [okx-api](/sdk/okx/javascript) must distinguish those two layers. One aggressive worker can lock the UID for every other process using the same account, even if each process thinks it is "under its own RPS cap".

## Monitoring Rate Limit Headers and 429 Error Responses {#monitoring-rate-limit-headers-and-429-error-responses}

You do not need to scrape raw Axios headers for every SDK. Two of them already surface what the exchange sent.

### Reading Binance and Bybit headers from the SDK {#reading-binance-and-bybit-headers-from-the-sdk}

The [binance](/sdk/binance/javascript) REST clients (`MainClient`, `USDMClient`, `CoinMClient`) update `x-mbx-used-weight`, `x-mbx-used-weight-1m`, `x-sapi-used-ip-weight-1m`, and the `x-mbx-order-count-*` headers after each response. Read the last seen values with `getRateLimitStates()`:

```ts title="Imported example"
import { MainClient } from "binance";

const client = new MainClient({
  api_key: process.env.BINANCE_API_KEY,
  api_secret: process.env.BINANCE_API_SECRET,
});

await client.getSymbolPriceTicker({ symbol: "BTCUSDT" });
console.log(client.getRateLimitStates());
```

Bybit does the same when you turn the parser on. Headers are `x-bapi-limit-status`, `x-bapi-limit`, and `x-bapi-limit-reset-timestamp`. They land on `response.rateLimitApi`:

```ts title="Imported example"
import { RestClientV5 } from "bybit-api";

const client = new RestClientV5({
  key: process.env.BYBIT_API_KEY,
  secret: process.env.BYBIT_API_SECRET,
  parseAPIRateLimits: true,
});

const response = await client.getPositionInfo({
  category: "linear",
  symbol: "BTCUSDT",
});

console.log(response.rateLimitApi);
// { remainingRequests, maxRequests, resetAtTimestamp }
```

OKX and Gate.io also expose account-level snapshots if you want an explicit read instead of inferring from headers:

```ts title="Imported example"
import { RestClient as OkxRestClient } from "okx-api";
import { RestClient as GateRestClient } from "gateio-api";

const okx = new OkxRestClient({
  apiKey: process.env.OKX_API_KEY,
  apiSecret: process.env.OKX_API_SECRET,
  apiPass: process.env.OKX_API_PASSPHRASE,
});
const okxLimits = await okx.getAccountRateLimit();

const gate = new GateRestClient({
  apiKey: process.env.GATE_API_KEY,
  apiSecret: process.env.GATE_API_SECRET,
});
const gateLimits = await gate.getAccountRateLimit();
```

If you still want one internal shape across venues, map from these SDK values rather than re-parsing headers yourself:

```ts title="Imported example"
interface RateLimitState {
  remaining: number;
  resetTimestamp: number;
  totalLimit: number;
}
```

`Retry-After` is useful when it is present. Binance includes it on some IP-weight 429s, and Gate.io typically includes it as well. Binance order-count 429s often omit it. Bybit frequently stays on HTTP 200 and returns `retCode` 10006. OKX often uses error code 50011. Your parser has to accept missing headers.

### Handling the 429 Too Many Requests Status {#handling-the-429-too-many-requests-status}

A 429 (or the venue's equivalent error code) means your local model is already wrong. Stop sending more of the same class of request. If the limit is IP-based, pause every process on that host. If it is UID-based, pause that account, not necessarily every key in the system.

Backoff should be exponential, with jitter, and it should honour `Retry-After` when the header exists. Log it as a production incident, not a retry footnote. After too many ignored 429s, Binance will answer with 418 and you are banned until the window ends.

The SDK still throws those responses at you. Catch them around the client call. Do not expect the client to sleep on your behalf.

## Implementation Patterns for Rate Limiting in Node.js {#implementation-patterns-for-rate-limiting-in-nodejs}

Reactive retries are a safety net. The actual design is a queue that knows the budget before the request leaves the process. On one machine an in-memory bucket is enough. Across several Node instances, put the counters in Redis so the sum of workers cannot exceed the key or IP cap.

### The Token Bucket Pattern for Burstable Orders {#the-token-bucket-pattern-for-burstable-orders}

A Token Bucket holds N tokens and refills at a constant rate. Each outbound call spends tokens equal to its cost: Binance weight, or 1 request for a Bybit V5 endpoint. If the bucket is empty, the call waits.

Here is a thin wrapper around [bybit-api](/sdk/bybit/javascript). The SDK submits the order. Your bucket decides when that is allowed. After the call, you can realign refill time with `rateLimitApi.resetAtTimestamp` so local state does not drift from the server.

```ts title="Imported example"
import { RestClientV5 } from "bybit-api";

const client = new RestClientV5({
  key: process.env.BYBIT_API_KEY,
  secret: process.env.BYBIT_API_SECRET,
  parseAPIRateLimits: true,
});

async function submitLinearOrder() {
  await waitForToken(); // your bucket, 1 token per request on this endpoint

  const response = await client.submitOrder({
category: "linear",
symbol: "BTCUSDT",
orderType: "Limit",
qty: "0.001",
side: "Buy",
price: "50000",
  });

  if (response.rateLimitApi) {
syncBucketFromExchange(response.rateLimitApi);
  }

  return response;
}
```

Keep a separate bucket per (exchange, UID, endpoint family). Mixing Bybit trade routes into the same bucket as Binance klines will either stall you early or blow a limit.

### Request Prioritization and Queue Management {#request-prioritization-and-queue-management}

Cancel-order traffic should beat balance polls and historical klines. When remaining capacity is low, pause market-data REST and keep the cancel / reduce-only path alive.

On Binance that is `cancelOrder` vs `getKlines`. On OKX it is `cancelOrder` vs public ticker reads. The [binance](/sdk/binance/javascript) and [okx-api](/sdk/okx/javascript) clients give you those methods typed. The priority queue still lives in your process.



## Building Resilient Integration Layers with Siebly SDKs {#building-resilient-integration-layers-with-siebly-sdks}

The SDKs exist so you are not hand-rolling HMAC, RSA, Ed25519, passphrases, and recvWindow handling for every venue. Package names on npm:

- [binance](/sdk/binance/javascript)
- [bybit-api](/sdk/bybit/javascript)
- [okx-api](/sdk/okx/javascript)
- [bitget-api](/sdk/bitget/javascript)
- [kucoin-api](/sdk/kucoin/javascript)
- [coinbase-api](/sdk/coinbase/javascript)
- [gateio-api](/sdk/gate/javascript)
- [bitmart-api](/sdk/bitmart/javascript)
- [@siebly/kraken-api](/sdk/kraken/javascript)
- [@siebly/htx-api](/sdk/htx/javascript)

[@siebly/kraken-api](/sdk/kraken/javascript) is a good example of why the wrapper belongs outside the client. Kraken's counter is not a weight header. You still call typed methods. You still own the throttle.

### Injecting Throttling Logic into SDK Workflows {#injecting-throttling-logic-into-sdk-workflows}

Check the bucket, then call the SDK. On Coinbase Advanced Trade the method is `submitOrder` on `CBAdvancedTradeClient`, not a generic "Place Order" helper:

```ts title="Imported example"
import { CBAdvancedTradeClient } from "coinbase-api";

const client = new CBAdvancedTradeClient({
  apiKey: process.env.API_KEY_NAME,
  apiSecret: process.env.API_PRIVATE_KEY,
});

async function placeLimitBuy() {
  await waitForToken();

  return client.submitOrder({
product_id: "BTC-USDT",
side: "BUY",
client_order_id: client.generateNewOrderId(),
order_configuration: {
limit_limit_gtc: {
base_size: "0.001",
limit_price: "50000.00",
},
},
  });
}
```

That same shape works for `bitmart-api`, `gateio-api`, and the others: wait, then call. Swap the bucket implementation without touching signing code.

Explore the full suite of [production-ready Siebly SDKs](/sdk).

## Scaling Trading Architecture Beyond Basic Throttling {#scaling-trading-architecture-beyond-basic-throttling}

REST throttling buys you time. It does not give you a live order book. If you poll tickers and depth on a timer, you will spend the budget that should be reserved for cancels and replaces. Subscribe once, then spend REST on execution.

### Transitioning to WebSockets for Market Data {#transitioning-to-websockets-for-market-data}

Public streams do not consume REST weight per update. [bitget-api](/sdk/bitget/javascript) and [kucoin-api](/sdk/kucoin/javascript) both expose a `WebsocketClient` (Bitget V3/UTA uses `WebsocketClientV3`):

```ts title="Imported example"
import { WebsocketClientV3, WS_KEY_MAP } from "bitget-api";

const wsClient = new WebsocketClientV3();

wsClient.on("update", (data) => {
  console.log("ticker", data);
});

wsClient.subscribe(
  {
topic: "ticker",
payload: {
instType: "spot",
symbol: "BTCUSDT",
},
  },
  WS_KEY_MAP.v3Public,
);
```

KuCoin Pro V2 is the same idea with a structured topic:

```ts title="Imported example"
import { WebsocketClient, WS_KEY_MAP } from "kucoin-api";

const client = new WebsocketClient();

client.on("update", (data) => {
  console.info("kline", data);
});

client.subscribe(
  {
topic: "kline",
payload: {
tradeType: "SPOT",
symbol: "BTC-USDT",
interval: "1min",
},
  },
  WS_KEY_MAP.spotPublicProV2,
);
```

Several venues also let you send trading commands over that same persistent socket. The SDK wraps that as `WebsocketAPIClient`: you `await` a method and get a typed response, without spending the REST window. That client exists today on [binance](/sdk/binance/javascript), [bybit-api](/sdk/bybit/javascript), [okx-api](/sdk/okx/javascript), `bitget-api`, `kucoin-api`, [gateio-api](/sdk/gate/javascript), [@siebly/kraken-api](/sdk/kraken/javascript) (spot), and [@siebly/htx-api](/sdk/htx/javascript). [coinbase-api](/sdk/coinbase/javascript) and [bitmart-api](/sdk/bitmart/javascript) do not ship a `WebsocketAPIClient` for order placement, so those two stay on REST plus public/private streams.

Bybit example, taken from the SDK's WS API examples:

```ts title="Imported example"
import { WebsocketAPIClient } from "bybit-api";

const wsClient = new WebsocketAPIClient({
  key: process.env.BYBIT_API_KEY,
  secret: process.env.BYBIT_API_SECRET,
});

const response = await wsClient.submitNewOrder({
  category: "linear",
  symbol: "BTCUSDT",
  orderType: "Limit",
  qty: "0.001",
  side: "Buy",
  price: "50000",
});
```

OKX uses an `accounts` array because one process can hold several keys:

```ts title="Imported example"
import { WebsocketAPIClient } from "okx-api";

const wsClient = new WebsocketAPIClient({
  accounts: [
{
apiKey: process.env.OKX_API_KEY,
apiSecret: process.env.OKX_API_SECRET,
apiPass: process.env.OKX_API_PASSPHRASE,
},
  ],
});

const res = await wsClient.submitNewOrder({
  instId: "BTC-USDT",
  tdMode: "cash",
  side: "buy",
  ordType: "market",
  sz: "100",
});
```

WebSocket APIs still have their own message and connection limits. They are not unlimited. They just stop you from burning the REST weight table for every heartbeat.

For how this sits in a larger system, see [algorithmic trading system architecture in Node.js](/blog/algorithmic-trading-system-architecture-in-nodejs-a-2026-engineering-guide).

### Circuit Breakers and Safety Boundaries {#circuit-breakers-and-safety-boundaries}

If 429s (or 10006 / 50011) cluster inside a short window, trip a circuit breaker and halt automated execution. That is how you avoid turning a soft reject into a Binance 418. Fail closed, alert, then resume with an empty queue and a fresh read of the limit headers.

## Engineering Resilience in High-Frequency Trading Systems {#engineering-resilience-in-high-frequency-trading-systems}

Model the venue, spend tokens before you send, and keep market data on sockets. That is the whole game: your Node process stays inside the published window instead of discovering the window via bans.

Siebly SDKs give you TypeScript REST and WebSocket clients, signing, and (on Binance and Bybit) parsed limit state. They will not run the Token Bucket for you. Put that layer in front of the client, then [pick the SDK for the venue you are wiring](/sdk).

## Frequently Asked Questions {#frequently-asked-questions}

### Do Siebly.io SDKs handle rate limiting automatically? {#do-siebly-io-sdks-handle-rate-limiting-automatically}

No. They do not queue, sleep, or shed traffic. You wrap the client. Two extras exist: [binance](/sdk/binance/javascript) tracks the last seen weight headers on `getRateLimitStates()`, and [bybit-api](/sdk/bybit/javascript) can attach `rateLimitApi` when `parseAPIRateLimits: true`. Requests made with `bybit-api` can also receive higher exchange-side limits because of the SDK's channel ID. That is an exchange policy, not a local throttle.

### What is the difference between a 429 error and an IP ban? {#what-is-the-difference-between-a-429-error-and-an-ip-ban}

A 429 (or Bybit `retCode` 10006, OKX 50011) is a rejected request. An IP ban is a network-level block. On Binance the ban arrives as HTTP 418 after you ignore 429s, and it can last from a couple of minutes to a few days. Gate.io public limits are IP-based and will decline bursts. That is not automatically a hours-long ban. Always read the current venue policy.

### How do I calculate request weights for the Binance API? {#how-do-i-calculate-request-weights-for-the-binance-api}

Read the weight column in the official Binance docs for that exact path, then confirm with `getRateLimitStates()` after a call. `getExchangeInfo()` has been listed as 10 and as 20 in different doc revisions. A new order is usually weight 1. Kline weight scales with `limit` and can reach 10 or 20. Do not copy a blog number into production without checking the live header.

### Can I use the same rate limiter for multiple exchanges? {#can-i-use-the-same-rate-limiter-for-multiple-exchanges}

Reuse the algorithm, not the instance. Give each venue its own bucket and refill rule. OKX windows, BitMart request caps, Binance weights, and Kraken counters cannot share one counter and stay correct.

### Is it better to throttle on the client side or wait for a 429 response? {#is-it-better-to-throttle-on-the-client-side-or-wait-for-a-429-response}

Throttle first. A 429 means an order may already have failed, and another 429 on Binance is how you earn a 418. Client-side queues also let you keep cancels moving while you shed kline polls.

### How do WebSockets affect my API rate limit usage? {#how-do-websockets-affect-my-api-rate-limit-usage}

Public streams stop you from polling REST for every ticker. WebSocket API order methods (`WebsocketAPIClient` on the SDKs listed above) keep execution off the REST weight table. Sockets still have subscribe, connection, and message caps. Use them, then still watch those caps.

### What are the best Node.js libraries for implementing a Token Bucket? {#what-are-the-best-node-js-libraries-for-implementing-a-token-bucket}

`limiter` and `bottleneck` are the usual starting points. For more than one process, store the tokens in Redis so every worker sees the same remaining budget. Point that queue at the SDK method. Do not fork the SDK to bake the queue in.

Disclaimer

*Technical and legal disclaimer: Siebly.io provides software development tools, SDKs, documentation, and educational engineering content for crypto exchange API integrations. This content is for software engineering education only and is not financial, investment, legal, tax, accounting, compliance, or trading advice.

Nothing in this article is a recommendation, invitation, or inducement to buy, sell, hold, trade, long, short, or allocate to any cryptoasset, exchange product, strategy, bot, or automated workflow. Examples, code patterns, simulations, backtests, and architecture diagrams are illustrative only and must not be treated as trading signals, investment recommendations, or evidence of future performance.

Cryptoasset markets are high risk and volatile. If you choose to build or operate exchange-connected software, you are responsible for your own legal, regulatory, tax, security, exchange-account, API-key, and risk-management obligations. Use public data, testnet, demo, dry-run, or paper-trading workflows before any live execution. Keep API keys server-side, use least-privilege permissions, and never enable withdrawals for automation keys unless you fully understand and accept the risks.

Siebly.io is not an exchange, custodian, investment adviser, trading-signal provider, or managed trading service. Official exchange documentation remains the source of truth for exchange-specific rules, API behavior, and terms of use. Use of Siebly.io content and software is also subject to the Siebly.io terms and conditions.*

## Related articles

- [Handling Exchange API Rate Limits in JavaScript: A 2026 Engineering Guide](/blog/handling-exchange-api-rate-limits-in-javascript-a-2026-engineering-guide)
- [Using proxy with Siebly SDKs](/blog/using-proxy-with-siebly-sdks)
- [Why Use a TypeScript SDK Instead of Raw Exchange API Requests?](/blog/typescript-sdk-vs-raw-exchange-api)


## Related Siebly Resources

- [Binance JavaScript SDK](/sdk/binance/javascript)
- [Bybit JavaScript SDK](/sdk/bybit/javascript)
- [OKX JavaScript SDK](/sdk/okx/javascript)
- [Siebly SDK directory](/sdk)
- [Exchange State Management](/ai/exchange-state)
