How to prevent duplicate orders in a trading bot

A request timeout does not mean an order failed. A webhook delivery does not mean it is new. A restarted worker does not know what happened while it was down. Safe bots assume events can repeat and acknowledgements can disappear, then make every trade intent converge on one durable outcome.

On this page
  1. Where duplicates come from
  2. Stable intent identity
  3. Safe submission flow
  4. Idempotent events and races
  5. Failure test matrix
  6. FAQ

Duplicate orders are usually a delivery problem

FailureNaive behaviorResult
Order call times outSend it again with a new IDBoth orders may be live
Webhook is retriedProcess every deliveryOne signal creates two intents
Two workers read one signalBoth pass the same pre-checkRace creates two orders
Process crashes after sendRestart assumes nothing happenedExisting venue order is repeated
Fill event is replayedAdd filled quantity againLocal exposure becomes wrong
Cancel/replace crossesSubmit replacement before old status is knownOld and new orders can both fill

Networks commonly provide at-least-once delivery or uncertain acknowledgement, not magical exactly-once effects across your database and a remote venue. The practical goal is idempotent processing: repeat the same input as often as necessary and still produce one logical effect.

Give every trade intent a stable identity

A signal is not yet an order. First create an intent containing the strategy version, instrument, side, desired exposure change, signal timestamp or bar, and risk decision. Give that intent a stable key. A deterministic key can be useful when one strategy must emit at most one action per symbol and bar; a generated unique ID works when multiple independent intents are valid. The business rule determines identity.

example intent keyintent_key = strategy_id
           + ":" + strategy_version
           + ":" + symbol
           + ":" + signal_bar_close
           + ":" + action

database UNIQUE(intent_key)

Generate the venue client order ID from or alongside that intent before the first network call, then persist both. Respect the venue's documented character, length, reuse, and retention rules. Never generate a fresh client order ID merely because the response was slow.

For TradingView webhook bots, require an event ID or derive a stable key from authenticated alert fields. Store it with a uniqueness constraint and return success for already-processed deliveries without placing another order.

Use a write-before-send submission flow

  1. Begin a local transaction and insert the unique intent.
  2. Reserve position and risk capacity so a second worker cannot use it.
  3. Create and store the client order ID with status SUBMITTING.
  4. Commit that evidence before calling the venue.
  5. Submit once with a bounded deadline.
  6. On a clear rejection, store the rejection and release the reservation.
  7. On acknowledgement, attach the venue order ID and move to the accepted state.
  8. On timeout or connection reset, move to UNKNOWN—not failed.
  9. Query by client ID or reconcile open and recent orders before any new send.
Unknown is a real order state

If the venue may have accepted a request, the only honest local status is unknown. Block conflicting intents for that instrument until a read or event resolves it. Treating unknown as rejected is the classic double-order bug.

The persistence layer and startup procedure are detailed in trading-bot state persistence. General retry classification belongs in error handling: safe market-data reads can often retry with backoff, while money-moving writes require reconciliation semantics.

Make fills, locks, and cancel-replace idempotent

Order submission is only half the problem. Private streams can redeliver events after reconnection, and a REST snapshot can overlap with streamed fills. Insert each venue fill using its stable fill or trade ID under a unique constraint. Recomputing the position from the fill ledger is safer than incrementing an unprotected variable every time a message arrives.

Cancel/replace needs its own state machine. A cancel request does not prove the old order is cancelled. It may fill while the cancellation is in flight. Wait for an authoritative terminal state, account for any late fill, recalculate remaining desired quantity, and then place the replacement. See partial fills and order reconciliation.

Test every ambiguity and race

InjectionExpected invariant
Same signal delivered 10 timesOne intent, at most one logical order
Two workers act simultaneouslyOne wins the atomic reservation
Venue accepts, response is droppedStatus becomes unknown; query finds original
Crash after send, before acknowledgement writeRestart reconciles by saved client ID
Same fill arrives via stream and snapshotFill posts once
Old order fills during cancelReplacement quantity is reduced or skipped
Venue client-ID lookup unavailableNew exposure stays blocked; operator alert fires

Monitor duplicate-intent conflicts, unknown-order age, unmatched venue orders, duplicate fill attempts, lock contention, and reconciliation differences. A rising dedupe count may indicate a noisy upstream retry policy even if financial exposure remains safe. Connect unresolved unknown orders to the kill-switch ladder.

Not financial advice. This content is educational. Automated and algorithmic trading carries a real risk of financial loss. Never trade money you cannot afford to lose. Review the SEC investor.gov and CFTC resources before trading.

Frequently asked questions

Why does a trading bot place duplicate orders?

Common causes are retrying an ambiguous timeout, receiving the same webhook or fill event twice, two workers acting on one signal, losing local state during restart, and cancel-replace races. The shared problem is treating delivery or acknowledgement as proof that an intent has happened exactly once.

What is an idempotent trading order?

An idempotent order workflow makes repeated processing of the same trade intent converge on the same logical order rather than creating additional exposure. It combines a stable intent key, a client order ID, durable status, uniqueness constraints, and reconciliation after uncertain results.

Should a bot retry an order after a timeout?

Not blindly. A timeout means the outcome is unknown: the venue may have accepted the order. Query by the original client order ID or reconcile recent orders and fills. Retry only under the venue's documented semantics and keep the same logical intent from producing a second order.

Is a unique client order ID enough to stop duplicate trades?

No. It helps only when it is created before the first send, persisted, reused or queried correctly, and supported by the venue's uniqueness rules. Local races, duplicate signals, expired ID windows, or a retry with a new ID can still create duplicates, so database constraints and exposure reconciliation are also required.

MB

Mustafa Bilgic

Algorithmic trading practitioner · Founder, AITradingBot.us

Mustafa builds and backtests automated trading systems and writes about them without the hype. Every tool on this site is free and runs entirely in your browser.