CCXT's common errors, and what they mean
Figures on this page are as of 2026-09. Fees, limits and margin tiers change — check the venue's own docs before acting on a number.
CCXT’s exception hierarchy is not a flat list of error names. It is a two-branch classification, and which branch an error lands in tells you whether retrying is safe. That is the most useful thing about it and the part most often missed.
The split at the top
BaseError
├── ExchangeError — the exchange understood you and refused
└── OperationFailed — the outcome is unknown
ExchangeError means the request arrived, was evaluated, and was rejected.
The request was wrong. Retrying it unchanged will fail again. Fix the request.
OperationFailed means something went wrong in transit or on the far side.
You do not know whether the request was processed.
Everything practical follows from this. For reads the distinction barely matters. For anything that changes state, it decides your entire error-handling strategy.
The ExchangeError branch
ExchangeError
├── AuthenticationError
│ ├── PermissionDenied
│ │ └── AccountNotEnabled
│ └── AccountSuspended
├── ArgumentsRequired
├── BadRequest
│ └── BadSymbol
├── OperationRejected
│ ├── NoChange
│ │ └── MarginModeAlreadySet
│ ├── MarketClosed
│ ├── ManualInteractionNeeded
│ └── RestrictedLocation
├── InsufficientFunds
├── InvalidAddress
│ └── AddressPending
├── InvalidOrder
│ ├── OrderNotFound
│ ├── OrderNotCached
│ ├── OrderImmediatelyFillable
│ ├── OrderNotFillable
│ ├── DuplicateOrderId
│ └── ContractUnavailable
├── NotSupported
├── InvalidProxySettings
└── ExchangeClosedByUser
Notes on the ones that confuse people:
AuthenticationError vs PermissionDenied. PermissionDenied is a
subclass. The first means the exchange does not accept who you are; the second
means it accepts who you are and will not let you do that. On Binance both can
surface from -2015, which covers bad key, wrong IP and missing permission in
one code. On Bybit they are better separated: 10005 is explicitly “Permission
denied, please check your API key permissions.”
RestrictedLocation is a geography refusal, not a bug. Bybit returns HTTP
403 for requests from US IPs, among other causes.
InsufficientFunds is worth catching separately. It is not a code problem
and not transient; something about your sizing or your margin state is wrong.
DuplicateOrderId connects to the retry story below, and does less than you
would hope — see the warning at the end.
The OperationFailed branch
OperationFailed
├── NetworkError
│ ├── DDoSProtection
│ ├── RateLimitExceeded
│ ├── ExchangeNotAvailable
│ │ └── OnMaintenance
│ ├── InvalidNonce
│ │ └── ChecksumError
│ └── RequestTimeout
├── BadResponse
│ └── NullResponse
└── CancelPending
RateLimitExceeded living under NetworkError is a deliberate and correct
classification. It is a back-off-and-retry condition, not a malformed request.
Venue examples: Binance -1003 TOO_MANY_REQUESTS (weight-based, escalating to
an IP ban signalled by HTTP 418), Bybit 10006 for account-level and 10018
for IP-level, OKX 50011.
InvalidNonce usually means clock skew rather than anything about nonces.
OKX rejects requests whose timestamp differs from server time by more than 30
seconds and recommends syncing via GET /api/v5/public/time; Binance returns
-1021 when the timestamp falls outside recvWindow.
RequestTimeout is the dangerous one, for reasons in the next section.
The part that matters for orders
For a read, an OperationFailed is an inconvenience: retry, get your data.
For createOrder, it means the outcome is unknown, and unknown is not the
same as failed. A timeout on an order placement could mean the request never
arrived, or that it arrived and filled and the response was lost. The client
cannot distinguish them.
So the rule is: never blind-retry an order after an OperationFailed.
Reconcile first — query open orders, recent fills and the position — then
decide.
And be careful with the standard mitigation. It is widely repeated that a
client-supplied order ID makes the retry idempotent. On Binance spot it does
not: newClientOrderId is documented as “a unique id among open orders”, and
orders reusing an ID “can be accepted only when the previous one is filled”.
That protects you while the original is resting and permits the duplicate once
it has filled — the exact case you needed protection for. Treat client order IDs
as reconciliation aids, not safety guarantees, and check each venue’s semantics
rather than generalising.
A handler shape that works
try:
order = exchange.create_order(...)
except ccxt.ExchangeError:
# Understood and refused. Do not retry unchanged.
raise
except ccxt.OperationFailed:
# Outcome unknown. Find out before doing anything.
reconcile(symbol, attempt_id, attempt_time)
Two properties worth preserving: catch ExchangeError and OperationFailed
rather than enumerating leaf classes, so new subclasses are handled sensibly;
and make reconcile the only path back to placing anything.
A caveat on the hierarchy itself
This is transcribed from CCXT’s ts/src/base/errors.ts — it is source code,
not an API contract. It is stable in practice, but re-check it on a major
version change rather than treating it as fixed.
FAQ
Should I retry on RateLimitExceeded?
For reads, yes, with backoff. For order placement, not without reconciling
first. CCXT classifies it under NetworkError and OperationFailed, meaning
the outcome is unknown — it was probably rejected at the gate, but “probably” is
doing too much work when the downside is a duplicate position.
What is the difference between ExchangeError and OperationFailed?
ExchangeError means the exchange received and understood the request and
refused it, so the request itself is wrong and retrying unchanged will fail
again. OperationFailed means something went wrong in transit, so you do not
know whether it was processed. The first tells you to fix something; the second
tells you to go and find out what happened.
Why am I getting InvalidNonce when I am not using nonces?
It is usually clock skew. Exchanges reject requests whose timestamp is too far
from their server time — OKX uses a 30-second window and returns 50102,
Binance returns -1021 when the timestamp falls outside recvWindow. Sync your
system clock, and on OKX consider syncing against GET /api/v5/public/time
before placing orders.
Can I rely on DuplicateOrderId to stop double-submits?
Not as a general guarantee. It depends on the venue’s uniqueness semantics, and on Binance spot those are narrower than people assume — the ID is unique only among open orders, so it can be reused after a fill. Reconcile before retrying rather than relying on the exchange to reject the duplicate.