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.
Calculate it with your own figures: API rate limit calculator.
Installation and symbols are in the CCXT Python guide.
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:
| 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_WEIGHTlimit in live SpotexchangeInfoon 5 November 2026. - Orders.
POST /api/v3/ordercosts 0.2, so the limiter allows 1,000 per 10 seconds againstORDERSof 100 per 10 seconds in the same response. - USD-M.
binanceusdmpaces cost 1 per weight at 50 ms: 1,200 a minute, half the 2,400 in itsexchangeInfothat 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.
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.
| 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.
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:
| 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_supporthas the same loop (4 POSTs). - Per-call override.
{"maxRetriesOnFailure": 0}inparamsworked on Binance, Bybit, Coinbase and Kraken. On OKX, where CCXT sends spot orders as a list tobatch-orders, it still made 4 POSTs and the key leaked into the body. - No pacing.
fetch2throttles once, before the loop, and the delay defaults to 0: after a mocked 429 withRetry-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.
| 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 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.
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")| 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.
For integration work in general, see exchange API integration or 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 (opens in a new tab)
- CCXT source at tag v4.5.92: errorHierarchy.ts (opens in a new tab)
- CCXT source at tag v4.5.92: exchange.py (fetch2, throttle, HTTP status map) (opens in a new tab)
- CCXT source at tag v4.5.92: binance.py (handle_errors, endpoint costs) (opens in a new tab)
- CCXT on PyPI: release history and MIT license (4.5.92 on 4 November 2026) (opens in a new tab)
- Binance Spot API documentation: REST API (newClientOrderId, HTTP return codes) (opens in a new tab)
- Binance Spot exchangeInfo, live rateLimits via the market data host (opens in a new tab)
- Binance USD-M Futures exchangeInfo, live rateLimits (opens in a new tab)
- Bybit V5 API: Place Order (orderLinkId) (opens in a new tab)
- Bybit V5 API: Error Codes (170141, 110072) (opens in a new tab)
- Coinbase Advanced Trade API: Create Order (client_order_id) (opens in a new tab)
- Kraken Spot REST API: Add Order (cl_ord_id, userref) (opens in a new tab)
- OKX API v5 documentation (place order, error code 51016) (opens in a new tab)
- Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish) (opens in a new tab)
- SPK (Capital Markets Board): Announcement on crypto-asset service providers, 5 August 2024 (Turkish) (opens in a new tab)
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.