# CCXT rate limit and error handling: what is safe to retry

URL: https://fincodelabs.com/en/blog/ccxt-rate-limit-error-handling · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Trading bots · Published: 2026-11-06 · Updated: 2026-11-06

CCXT rate limit and error handling in release 4.5.92: what rateLimit does, which exceptions are safe to retry, why orders are not, and a tested wrapper.

**Short answer:** The **CCXT rate limit** is a client-side pause, on by default, of `rateLimit` milliseconds times the endpoint's cost, kept separately by each exchange instance. In **CCXT error handling**, a fetch can be retried with backoff, but a `create_order` that ends in any `OperationFailed` has an unknown outcome and must be looked up before any resend. In offline tests of release 4.5.92 on 5 November 2026, the `maxRetriesOnFailure` option made no such distinction: set to 3, it turned one timed-out Binance order call into 4 POSTs.

Installation and symbols are in the [CCXT Python guide](https://fincodelabs.com/en/blog/ccxt-python-trading-bot).

Facts come from the CCXT manual and the source of release v4.5.92 (4 November 2026), read on 5 November 2026. CCXT is MIT-licensed. Tests ran offline with placeholder keys, so they show what CCXT sends and raises, not how a live venue answers. No exchange is recommended, automated trading can lose money, and availability depends on your jurisdiction and the platform's terms. Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized. Being on the Capital Markets Board's list does not mean authorization.

## CCXT rate limit: what rateLimit and enableRateLimit do

`rateLimit` is in milliseconds, the pause before a request is `rateLimit` x the endpoint's cost, and `enableRateLimit`, on by default, switches the pause on.

Defaults in 4.5.92: `rateLimit` is 50 ms on Binance, 20 on Bybit, 34 on Coinbase, 1,000 on Kraken and 110 on OKX; the algorithm is `leakyBucket`; `timeout` is 10,000 ms. The manual gives `rollingWindow` a 60 s window, but it is 5,000 ms on Bybit, 10,000 on Kraken and 0 on OKX. Measured with `rateLimit` 500 and cost 0.2, so 100 ms apart is expected:

**Pacing of 11 calls, CCXT 4.5.92, stubbed transport, five runs, 5 November 2026**

| Client and setting | Total | Pattern |
| --- | --- | --- |
| Sync, default | 1,004 to 1,007 ms | About 100 ms apart |
| Sync, `rollingWindow`, 1,000 ms window | 1,004 to 1,006 ms | The same: no effect |
| Async, default leaky bucket | 1,001 to 1,014 ms | About 100 ms apart |
| Async, `rollingWindow`, 1,000 ms window | 1,005 to 1,016 ms | 10 at once, then a wait of about 1 s |

In Python the sync class's `init_throttler` is a stub and `throttle` only spaces requests, so `rollingWindow` needs `ccxt.async_support`.

## CCXT Binance rate limit: what the costs cover and miss

CCXT's Binance Spot costs are request weight x 0.2, so the default pace can use Binance's whole weight budget, and its order counter is not modeled.

- **Weight.** 50 ms x 0.2 = 10 ms per unit, or 6,000 a minute, the `REQUEST_WEIGHT` limit in live Spot `exchangeInfo` on 5 November 2026.
- **Orders.** `POST /api/v3/order` costs 0.2, so the limiter allows 1,000 per 10 seconds against `ORDERS` of 100 per 10 seconds in the same response.
- **USD-M.** `binanceusdm` paces cost 1 per weight at 50 ms: 1,200 a minute, half the 2,400 in its `exchangeInfo` that day.
- **Instances.** Each instance has its own limiter; the manual says not to use several with one key from one IP. With the same stub, 15 calls took a median 1,408 ms on one async instance, 409 ms on three, and 503 ms from three threads sharing one unlocked sync instance (15 runs each).

> **Hypothetical example.** To keep 20% of Spot's weight spare, set `rateLimit` to 50 / 0.8 = 62.5.

Binance's own counters are in [Binance API rate limit](https://fincodelabs.com/en/blog/binance-api-rate-limit).

## CCXT error handling: which exception means what

`ExchangeError` means the request was wrong or refused, so fix it first; `OperationFailed` means the problem may pass or the result is in doubt.

**CCXT exceptions, from errorHierarchy.ts and offline tests of 4.5.92, 5 November 2026**

| Exception | Family | Meaning | Action |
| --- | --- | --- | --- |
| `RequestTimeout` | NetworkError | No complete reply in time; also HTTP 408, 504 | Fetch: back off. Order: look up |
| `ExchangeNotAvailable` | NetworkError | Unreachable; also HTTP 500, 502 and, with no better match, 400, 403, 404, 451 | Same; stop on 403 or 451 |
| `DDoSProtection`, `RateLimitExceeded` | NetworkError | Protection layer, or over the limit | Everything waits for `Retry-After` |
| `InvalidNonce` | NetworkError | Nonce or clock, such as Binance `-1021` | Fix the clock; do not loop |
| `NetworkError`, `OperationFailed`, `BadResponse` | OperationFailed | Dropped connection, unreadable reply, Binance `-1000`, `-1006` | Fetch: back off. Order: look up |
| `InvalidOrder`, `BadRequest` | ExchangeError | Wrong size, price, parameter | Fix it, unless the message is a duplicate ID: then look up |
| `OrderNotFound` | InvalidOrder | After a cancel: canceled or filled | Fetch to find out which |
| `AuthenticationError`, `PermissionDenied` | ExchangeError | Key, permission or account | Stop and alert |
| `RestrictedLocation`, `MarketClosed` | OperationRejected | Refused by a rule or state | Do not loop; check the rule |

**The Binance trap.** `binance.py` raises `DDoSProtection` for HTTP 429 and 418, so `except ccxt.RateLimitExceeded` missed a mocked 429, although the base class maps 429 to that class. Catch both. CCXT's REST calls ignore `Retry-After`, but `ex.last_response_headers` holds it.

**Blocks are not faults.** A regional block (HTTP 403 or 451) is the platform's rule, not a bug to work around: stop, and check whether you may use that platform.

**Where the documents disagree.** The manual's diagram puts `BadResponse` under `ExchangeError` and `CancelPending` under `InvalidOrder`, and its text makes `RateLimitExceeded` a sub-type of `DDoSProtection`; `errorHierarchy.ts` puts the first two under `OperationFailed` and makes the last two siblings. One passage says `OperationFailed` "can be blindly re-tried"; its `RequestTimeout` section says otherwise.

## CCXT RequestTimeout and connection timeouts: what is safe to retry

Retry a fetch, repeat a cancel, and look up an order before any resend: a timeout says nothing about whether the exchange processed the request.

- `fetch_*`: retry with backoff; the manual calls this safe.
- `cancel_order`: call it again.
- `create_order`: check open and closed orders, then the balance, as the manual says.

`ccxt.base.errors.RequestTimeout` came from both connect and read timeouts in testing. The doubt is wider than this class: Binance's REST documentation, read 5 November 2026, says that after HTTP 5XX "the execution status is **UNKNOWN**". In the mock, HTTP 500 and 502 on order calls raised `ExchangeNotAvailable`, `-1000` and `-1006` raised plain `OperationFailed`, and HTML with HTTP 200 raised nothing and returned an order with `id` `None`.

## CCXT maxRetriesOnFailure resends orders: the test

The option resends orders: `fetch2` applies it to any `OperationFailed` on any endpoint, and the manual's Retry Mechanism section exempts only `InsufficientFunds` and `InvalidOrder` and is silent on orders.

```python
import ccxt, requests
from requests.adapters import BaseAdapter
from urllib.parse import parse_qs

class NoReply(BaseAdapter):
    def __init__(self):
        super().__init__()
        self.posts = []
    def send(self, request, **kwargs):
        self.posts.append(parse_qs(request.body.decode())["newClientOrderId"][0])
        raise requests.exceptions.ReadTimeout("no reply")
    def close(self):
        pass

ex = ccxt.binance({"apiKey": "placeholder", "secret": "placeholder"})
ex.set_markets([{"id": "BTCUSDT", "symbol": "BTC/USDT", "spot": True, "limits": {},
                 "precision": {"amount": 0.00001, "price": 0.01},
                 "info": {"orderTypes": ["LIMIT"]}}])
ex.session.mount("https://", net := NoReply())
ex.options["maxRetriesOnFailure"] = 3

try:
    ex.create_order("BTC/USDT", "limit", "buy", 0.001, 50000, {"clientOrderId": "demo-001"})
except ccxt.RequestTimeout:
    print("RequestTimeout after", len(net.posts), "POSTs:", net.posts)
```

Output on 4.5.92:

```
RequestTimeout after 4 POSTs: ['demo-001', 'demo-001', 'demo-001', 'demo-001']
```

A longer fake, not shown, applied Binance's rule that an ID is reusable after a fill:

**The option at 3 on that fake**

| Scenario | POSTs | Orders | Caller sees |
| --- | ---: | ---: | --- |
| No attempt reaches the venue | 4 | 0 | `RequestTimeout` |
| First POST accepted and resting, reply lost | 2 | 1 | `InvalidOrder`: Duplicate order sent. |
| Every POST filled at once, replies lost | 4 | 4 | `RequestTimeout` |

In the second row the caller reads a rejection while the order is live.

- **Default and async.** The default of 0 sent 1 POST; `ccxt.async_support` has the same loop (4 POSTs).
- **Per-call override.** `{"maxRetriesOnFailure": 0}` in `params` worked on Binance, Bybit, Coinbase and Kraken. On OKX, where CCXT sends spot orders as a list to `batch-orders`, it still made 4 POSTs and the key leaked into the body.
- **No pacing.** `fetch2` throttles once, before the loop, and the delay defaults to 0: after a mocked 429 with `Retry-After: 60`, three more sends followed within milliseconds, which Binance's documentation says earns an IP ban.

CCXT's own order router takes the opposite line: an unknown outcome "is **never** retried".

## A repeated client order ID protects only where duplicates are refused

The same ID on a resend is no guarantee: uniqueness rules, and how CCXT passes the ID, differ by exchange.

**Client order IDs: 4.5.92 tested offline, official pages read 5 November 2026**

| Exchange | Sent if you pass none | Your ID goes in | Official page says |
| --- | --- | --- | --- |
| Binance Spot | Generated `x-...` | `clientOrderId` | Unique among open orders; reusable after a fill |
| Bybit | Nothing | `clientOrderId` | "always unique"; errors 170141 and 110072 name duplicates; scope not stated |
| Coinbase | Generated `ccxt-...` | `client_order_id`; `clientOrderId` was dropped | Not unique: no new order, the existing one is returned |
| Kraken Spot | Nothing | `clientOrderId` | Identifies an open order; reply to a duplicate not stated |
| OKX | Generated | `clientOrderId` | Unique among pending orders, reusable after a final state; error 51016 names duplicates |

Always pass your own ID: the exception does not carry a generated one to search for. Where nothing is sent, each resend looks like a new order. An ID that is free again after a fill protects a resting order but not one that filled at once. See the [client order ID guide](https://fincodelabs.com/en/blog/client-order-id) for IDs that never repeat.

## A tested wrapper: fetches back off, orders look first

Leave `maxRetriesOnFailure` at 0, retry reads in one function, send each order once, and look before anything else.

```python
import random, time
import ccxt

class OutcomeUnknown(Exception):
    """The send may have reached the exchange: stop new orders."""

def retry_after(ex, err):
    if isinstance(err, (ccxt.DDoSProtection, ccxt.RateLimitExceeded)):
        value = str((ex.last_response_headers or {}).get("Retry-After", "")).strip()
        if value.isascii() and value.isdigit():
            return float(value)
    return 0.0

def read(call, *args, tries=4):
    """Unified fetch methods only."""
    for attempt in range(tries):
        try:
            return call(*args)
        except ccxt.OperationFailed as err:
            wait = max(random.uniform(0, min(8, 0.5 * 2 ** attempt)), retry_after(call.__self__, err))
            text = str(err)
            stop = isinstance(err, ccxt.InvalidNonce) or " 403 " in text or " 451 " in text
            if stop or wait > 60 or attempt == tries - 1:
                raise
            time.sleep(wait)

def find_order(ex, symbol, client_id, since):
    found = {o["id"]: o for fetch in (ex.fetch_open_orders, ex.fetch_closed_orders)
             for o in read(fetch, symbol, since) if o.get("clientOrderId") == client_id}
    if len(found) > 1:                       # id reused: no safe answer
        raise OutcomeUnknown(f"{client_id}: {len(found)} orders carry it")
    return next(iter(found.values()), None)

def place_order(ex, symbol, side, amount, price, client_id, looks=3, pause=2.0):
    if ex.options.get("maxRetriesOnFailure") or not client_id:
        raise ValueError("need maxRetriesOnFailure at 0 and a new client id")
    field = "client_order_id" if ex.id == "coinbase" else "clientOrderId"
    since = ex.milliseconds() - 60000        # skip older orders, allow clock skew
    try:
        order = ex.create_order(symbol, "limit", side, amount, price, {field: client_id})
        if order.get("id"):                  # HTML with HTTP 200: empty order
            return order
        cause = "empty reply"
    except ccxt.OperationFailed as err:      # timeout, 5xx, 429: outcome unknown
        cause, pause = type(err).__name__, max(pause, retry_after(ex, err))
    except ccxt.InvalidOrder as err:         # Binance -2010: this id is already live
        if "Duplicate" not in str(err):
            raise
        cause = "duplicate id"
    try:
        for _ in range(looks if pause <= 60 else 0):   # longer: a ban, so stop
            time.sleep(pause)
            if found := find_order(ex, symbol, client_id, since):
                return found
    except ccxt.BaseError as err:
        cause += f", lookup {type(err).__name__}"
    raise OutcomeUnknown(f"{client_id}: {cause}, unresolved")
```

**Wrapper results on the fake venue, sleeps stubbed**

| Fault | Result | POSTs | Orders |
| --- | --- | ---: | ---: |
| Accepted, reply lost: resting, or visible on the second look | Found: open | 1 | 1 |
| Accepted, reply lost, filled | Found: closed | 1 | 1 |
| HTTP 429, `Retry-After: 7` | `OutcomeUnknown` after three 7 s waits | 1 | 0 |
| Connect timeout; HTML with HTTP 200 | `OutcomeUnknown` | 1 | 0 |
| Insufficient balance | `InsufficientFunds` | 1 | 0 |
| HTTP 418, `Retry-After: 120` | `OutcomeUnknown`, no wait | 1 | 0 |

`read()` sent 4 GETs on HTTP 502, 1 on 403 or 451, waited 7 s after a 429, and stopped when `Retry-After` asked for 600 s. An ID used an hour earlier was skipped and an empty ID refused, but one filled 10 s earlier came back as this order's fill if the new send never arrived.

`OutcomeUnknown` after an empty or failed lookup is deliberate: the lookup cannot separate "never arrived" from "not visible yet". Stop new orders, alert someone, compare balances, and only then resend with the same ID. Lookups cost Binance Spot weight 6 and 20, and support varies: check `ex.has`. What happens once stopped is in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

For integration work in general, see [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration) or [trading bot code review](https://fincodelabs.com/en/services/trading-bot-code-review); naming Binance, Bybit, Coinbase, Kraken or OKX here does not mean Fincode integrates them or has built on them.

## Frequently asked questions

### Why do I still get rate limit errors with enableRateLimit on?

The limiter paces one exchange instance; other instances, processes and programs on the IP are not counted, nor are order counters.

### What does ccxt.base.errors.RequestTimeout mean?

No complete reply arrived within the timeout, 10,000 ms by default. The request may still have been processed: look up an order before resending it.

### Can I retry create_order in CCXT?

Look up open and closed orders by client order ID first, and leave maxRetriesOnFailure at 0: in 4.5.92 it also resends orders.

### Why does except ccxt.RateLimitExceeded not catch a Binance 429?

In 4.5.92, binance.py raises DDoSProtection, a sibling class, for HTTP 429 and 418. Catch both.

## Sources

- [CCXT Manual at tag v4.5.92: Rate Limit, Error Handling, Retry Mechanism](https://github.com/ccxt/ccxt/blob/v4.5.92/wiki/Manual.md)
- [CCXT source at tag v4.5.92: errorHierarchy.ts](https://github.com/ccxt/ccxt/blob/v4.5.92/ts/src/base/errorHierarchy.ts)
- [CCXT source at tag v4.5.92: exchange.py (fetch2, throttle, HTTP status map)](https://github.com/ccxt/ccxt/blob/v4.5.92/python/ccxt/base/exchange.py)
- [CCXT source at tag v4.5.92: binance.py (handle_errors, endpoint costs)](https://github.com/ccxt/ccxt/blob/v4.5.92/python/ccxt/binance.py)
- [CCXT on PyPI: release history and MIT license (4.5.92 on 4 November 2026)](https://pypi.org/project/ccxt/)
- [Binance Spot API documentation: REST API (newClientOrderId, HTTP return codes)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot exchangeInfo, live rateLimits via the market data host](https://data-api.binance.vision/api/v3/exchangeInfo?symbol=BTCUSDT)
- [Binance USD-M Futures exchangeInfo, live rateLimits](https://fapi.binance.com/fapi/v1/exchangeInfo)
- [Bybit V5 API: Place Order (orderLinkId)](https://bybit-exchange.github.io/docs/v5/order/create-order)
- [Bybit V5 API: Error Codes (170141, 110072)](https://bybit-exchange.github.io/docs/v5/error)
- [Coinbase Advanced Trade API: Create Order (client_order_id)](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/create-order)
- [Kraken Spot REST API: Add Order (cl_ord_id, userref)](https://docs.kraken.com/api/docs/rest-api/add-order/)
- [OKX API v5 documentation (place order, error code 51016)](https://www.okx.com/docs-v5/en/)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)
- [SPK (Capital Markets Board): Announcement on crypto-asset service providers, 5 August 2024 (Turkish)](https://spk.gov.tr/duyurular/basin-duyurulari/2024/kripto-varlik-hizmet-saglayicilara-iliskin-duyuru_05082024)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/ccxt-rate-limit-error-handling

Cite as: Fincode, "CCXT rate limit and error handling: what is safe to retry", updated November 6, 2026, https://fincodelabs.com/en/blog/ccxt-rate-limit-error-handling
