Learn / Exchange APIs

Retrying a failed order is not safe

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.

A network error on an order-placing call does not mean the order failed. It means you do not know what happened — and for an order, “do not know” and “failed” are not the same thing.

This is the most expensive misunderstanding in automated trading, and the standard mitigation for it does not work the way people think.

The two kinds of failure

CCXT’s exception hierarchy splits at the top, and the split is the whole point:

BranchMeaningSafe to retry unchanged?
ExchangeErrorThe exchange understood the request and refused itNo — it will refuse again
OperationFailedSomething went wrong in transitUnknown

Under OperationFailed sits NetworkError, and under that RequestTimeout, RateLimitExceeded, ExchangeNotAvailable and DDoSProtection.

For a read, that branch is a nuisance: retry, get your data, move on. For createOrder it is a different category of problem, because the failure is not “the order was rejected”. It is “the response did not come back”. Those have very different possible causes:

  • The request never reached the exchange. Nothing happened.
  • The request arrived, was rejected, and the rejection was lost. Nothing happened.
  • The request arrived, was accepted, sits resting. Something happened.
  • The request arrived, was accepted, filled immediately, and the response was lost. Something definitely happened.

The client sees the same timeout in all four cases.

Why the naive handler is dangerous

Standard practice is to retry on network errors, because that is correct for almost everything else. Applied to order placement, the third and fourth cases turn into a doubled position.

Worse, this is a failure that hides. The retry succeeds, you have a position, and nothing in the logs says “you placed this twice” — there are just two fills. You find out from your exposure.

The client order ID does not fix this

The usual advice at this point is: use a client-supplied order ID, and the exchange will reject the duplicate.

On Binance spot, this is not what happens. From the trading endpoints documentation, on newClientOrderId:

“A unique id among open orders. Automatically generated if not sent. Orders with the same newClientOrderID can be accepted only when the previous one is filled, otherwise the order will be rejected.”

Read the scope: unique among open orders. So:

  • If the original is still resting, the retry is rejected. Protection works.
  • If the original has filled, the same ID is accepted again. You get a second position.

Which means the mechanism protects you in the case you did not need protecting from, and fails in the case that matters most — a timeout on an order that filled instantly. Market orders, the ones most likely to fill before the response returns, are exactly where it offers least.

This is not a Binance flaw. newClientOrderId was designed as an identifier for tracking open orders, not as an idempotency key, and the documentation says so plainly. The error is in the advice that repurposes it as one.

Other venues differ, and none of them should be assumed. If you are relying on client order IDs for retry safety on a venue, find the sentence in its documentation that says what uniqueness means and over what window. If you cannot find it, you do not have the guarantee.

What to do instead

Reconcile, then decide. On any ambiguous failure of an order call:

  1. Stop. Do not send anything.
  2. Query open orders for the symbol.
  3. Query recent fills or trades, covering the window around the attempt.
  4. Query the position.
  5. Only now decide whether to place anything.

It costs three requests and a second of latency, and it converts a guess into a fact.

Design so reconciliation is possible. Always send a client order ID — not for idempotency, but so that step 2 can identify your attempt among orders that may exist for other reasons. Record the timestamp of the attempt so step 3 has a window to search. These are cheap at write time and invaluable at recovery time.

Treat rate limits as ambiguous too. RateLimitExceeded sits under NetworkError in CCXT for good reason. It usually means the request was rejected at the gate. “Usually” is not a property you want governing whether a retry doubles a position.

Never auto-retry order placement. Auto-retry reads freely. For writes, the retry is a decision, and it needs the reconciliation above first.

The MCP angle

If a model is placing the orders, this gets worse in a specific way: a failed tool call comes back as a result the model reasons about, and the natural next move for a model looking at an error is to try again. It has no concept that this particular tool is unsafe to repeat.

The protocol anticipates this. MCP tool annotations include idempotentHint, described in the schema as whether “calling the tool repeatedly with the same arguments will have no additional effect on the its environment” — and it defaults to false. For an order-placement tool that default is correct. Any client or wrapper that retries tool calls should respect it.

But remember that annotations are hints the server makes about itself; the schema states they “are not guaranteed to provide a faithful description of tool behavior”. The reliable place to put retry logic is in code you control, not in a flag you read.

FAQ

If I get a timeout, was the order placed?

Unknown, and that is the whole problem. The timeout tells you a response did not arrive; it says nothing about whether the request did. Query open orders, recent fills and your position before sending anything. The answer takes three requests and removes the guesswork entirely.

Doesn’t a client order ID make retries idempotent?

Not on Binance spot, and do not assume it elsewhere. Binance documents newClientOrderId as “a unique id among open orders”, and orders reusing an ID “can be accepted only when the previous one is filled”. So a retry is blocked while the original rests and permitted once it has filled — which is precisely the scenario where a duplicate costs you. Use the ID for reconciliation, not for safety.

Is it safe to retry after a rate-limit error?

Not without reconciling. CCXT places RateLimitExceeded under NetworkError and OperationFailed, the branch meaning the outcome is unknown. The order was probably rejected at the gate, but the downside of “probably” here is a doubled position.

What about cancel and amend — are those safe to retry?

Much safer, because they are closer to idempotent in effect: cancelling an already-cancelled order is typically an error rather than a harmful action. Still check the venue’s semantics, and be careful with amend, which on some venues is implemented as cancel-and-replace and can leave you briefly unprotected if it half-completes.