How to Handle Failed and Rejected Orders in a Trading Bot

Failed and rejected orders are normal in automated trading. Learn to classify each error, retry transient failures idempotently, skip permanent rejections, and reconcile state so one signal means one order.

How to Handle Failed and Rejected Orders in a Trading Bot

An automated strategy can be flawless on the chart and still lose the trade at the last step, when the exchange refuses the order. Learning to handle failed orders in a trading bot is what separates a demo that works on a quiet afternoon from infrastructure you can leave running unattended. A single unhandled rejection can leave your real position out of sync with what your strategy believes it holds. This guide breaks down why orders fail, how to tell one kind of failure from another, and how to respond to each without making things worse. The goal is not to eliminate failures, which is impossible, but to make them predictable โ€” a good bot treats a rejected order as normal input, not an emergency.

Failed vs Rejected: Two Different Problems

It helps to separate two situations that feel similar but need opposite responses. A rejected order reached the exchange and the exchange said no: insufficient balance, price outside limits, an untradable symbol, or a key without trade permission. The exchange made a clear decision, and you know your order did not execute.

A failed order is worse because it is ambiguous. The request timed out or the connection dropped, and you do not know whether the exchange processed the order or never saw it. Treating an ambiguous failure like a clean rejection is the most common way bots place the same trade twice or miss a fill they actually got. Every error-handling decision starts with one question: did the exchange decide, or do I simply not know?

Why Orders Fail or Get Rejected

Most rejections come from a short list of causes: insufficient balance (often from fees or an earlier order that consumed the funds); price and quantity filters that reject orders below a minimum notional value or outside the exchange's tick and lot size rules; rate limits that reject bursts of calls; and permission errors from a key created without trade access or recently rotated.

Failures, by contrast, are about the network and the exchange's load. Timeouts spike during volatile periods, exactly when your signals are most likely to fire. Because the two categories have different root causes, the first job of your bot is to sort each error into the right bucket.

Step 1: Classify the Error Before You React

Before retrying, canceling, or alerting, classify the response. Exchanges signal intent through HTTP status codes and error codes, and your bot should read both. A `400`-class response with a specific error code is a deliberate rejection: retrying it unchanged will only be declined again. A `429` means you are rate limited and should back off, not stop. A `5xx` or a timeout is a server-side or network failure where the outcome is unknown.

Map each provider's error codes to a small set of internal categories your logic understands: `permanent_reject`, `rate_limited`, `retryable_transient`, and `unknown_state`. Rejections tied to filters and balances belong in `permanent_reject`; timeouts and `5xx` belong in `unknown_state`. Getting this classification right matters more than any single retry setting, because the category decides everything that follows.

Step 2: Retry the Right Way

Retries are useful only for transient problems, and only when done carefully. Two techniques make them safe. The first is exponential backoff with jitter: wait a short, growing, slightly randomized interval between attempts so you do not hammer a struggling exchange, and so a fleet of bots does not retry in lockstep. Cap the attempts; three is a sensible ceiling, after which you escalate rather than loop forever.

The second, and more important, technique is idempotency. When you resubmit an order whose fate is unknown, you risk creating a duplicate. Most exchanges let you attach a client order ID, a unique identifier you generate for each intended trade. If a retry carries the same client order ID as the original, the exchange recognizes it and will not create a second order. Derive that ID deterministically from the signal so a retry always produces the same value. Idempotency turns a dangerous "did it go through?" retry into a safe one. Without it, retrying an `unknown_state` order is a gamble; with it, retrying is simply the correct move.

Step 3: Know When Not to Retry

A resilient bot is defined as much by the retries it refuses as by the ones it makes. Never retry a `permanent_reject` unchanged, because the answer will not change. If the order was rejected for insufficient balance, retrying will not create funds; record the miss and move on. If it violated a price or size filter, correct the order rather than resend the broken one. If a key lacks trade permission, no amount of retrying helps, and the right response is to alert you so you can fix the key.

There is also a timing rule that matters more in trading than in most software: a signal has a shelf life. Retrying a market order thirty seconds later can fill you at a price your strategy never intended, so build a maximum age into your retry logic and let old signals expire rather than execute against a market that has already moved. This is closely tied to slippage; our guide on how to reduce slippage in automated crypto trading covers why late fills quietly erode a strategy.

Step 4: Reconcile State and Make Failures Visible

After an `unknown_state` failure, your records and the exchange can disagree, so reconcile before you act again. Query the exchange for the order by its client order ID, or check current positions and open orders, and update your internal state to match what the exchange reports rather than what you assumed. Reconciliation is how you recover from the ambiguous failures that idempotency alone cannot fully resolve.

Just as important, no failure should happen silently. A bot that swallows errors drifts out of sync until you notice by accident, usually at the worst moment. Log every rejection and failure with enough context to debug it: the signal, the classified category, the exchange response, and what the bot decided to do. Route the failures that need a human, such as permission errors or repeated timeouts, to an alert you will actually see. The difference between a hobby script and dependable automation is not that failures stop; it is that every one is caught and either handled or surfaced clearly.

Handling Partial Fills and Ambiguous States

Not every order is cleanly filled or cleanly rejected. A limit order can partially fill and leave a remainder resting on the book, and a bot that assumes all-or-nothing will misjudge its position. Decide in advance what a partial fill means for your strategy: cancel the remainder and treat the filled amount as the position, or leave it working for a defined window. Either way, read the actual filled quantity from the exchange rather than assuming your requested size executed. Our explainer on how crypto exchange order books work gives useful background on how resting orders behave.

Best-Practices Checklist

  • Classify every response into a clear category before deciding to retry, cancel, or alert.
  • Attach a deterministic client order ID to every order so retries are idempotent and cannot duplicate a trade.
  • Use exponential backoff with jitter and a hard cap on attempts, typically no more than three.
  • Never retry a permanent rejection unchanged; fix the order or record the miss instead.
  • Expire stale signals rather than executing them late against a moved market.
  • Reconcile against the exchange after any unknown-state failure and trust its records over your own.
  • Log every failure with full context and alert on the ones that need a human.
  • Test against real error conditions, not just the happy path, before connecting live funds.

Where a Relay Layer Fits

Building all of this yourself means running an always-on service, mapping every exchange's error codes, and getting idempotency and reconciliation right under load. That is a lot of infrastructure to own before you place a single reliable trade.

This is the layer SignalToExchange is built to handle. As a non-custodial webhook relay, it receives your signal, classifies exchange responses, retries transient failures idempotently, and reconciles state, so one signal results in exactly one order. Your funds stay on your own exchange, connected through trade-only API keys with no withdrawal access. You keep the strategy and the logic; the relay absorbs the failure handling between a signal and a confirmed order. If you are moving from a script to automation you can leave running, request access / start your free trial today. For the broader picture, see our TradingView webhook to exchange setup guide.

Frequently Asked Questions

What is the difference between a failed and a rejected order?

A rejected order reached the exchange and was declined for a clear reason, such as insufficient balance or a price outside the allowed range, so you know it did not execute. A failed order timed out or lost the connection with no response, so the outcome is unknown. A rejection is final; a failure requires you to check what actually happened before acting.

Should my trading bot automatically retry failed orders?

Only for transient failures like timeouts and rate limits, and only with safeguards: exponential backoff with jitter, a cap on attempts, and a client order ID so a retry cannot create a duplicate. Do not automatically retry a deliberate rejection, because the exchange will decline the same request again; fix the underlying issue or record the miss instead.

How do I stop a retry from placing a duplicate trade?

Use idempotency. Generate a unique client order ID for each intended trade, derived deterministically from the signal, and send that same ID on every retry. Exchanges that support client order IDs recognize the repeat and refuse to create a second order, so an ambiguous retry becomes safe.

What should happen when an order is only partially filled?

Read the actual filled quantity from the exchange rather than assuming your full size executed, then apply a rule you set in advance: cancel the resting remainder and treat the filled amount as your position, or leave it working for a defined window. Base your state on what the exchange reports, not on what you requested.

Automated trading involves risk. SignalToExchange is execution infrastructure and does not provide financial advice, trading signals, or guarantees of any kind.

Secure Signal Routing Infrastructure

Non-custodial execution. Trade-only API keys. Independent infrastructure built for reliability.

Request Early Access

Trade-only API key enforcement. No withdrawal permissions. No custody.