Short answer: A client order ID is a label your software assigns to an order before sending it, so that after a timeout it can ask the venue whether the order exists instead of sending it twice. In FIX it is ClOrdID, tag 11. The label alone does not make a retry safe: as documented on 5 November 2026, Coinbase returns the existing order for a repeated ID, while Binance, Kraken and OKX enforce uniqueness only among open orders, so a retry after the first order has filled can create a second order.
If an order request times out, can the software send it again without creating a second order? The answer differs by venue. Order states are in the crypto order management system guide and the general timeout sketch in trading bot development.
All rules were read on official pages on 5 November 2026. Format checks ran on the public Binance Spot Testnet and one lookup on the Hyperliquid testnet, without real keys, so the retry trap ran on a synthetic venue that applies the documented rules. Naming a venue is not a recommendation. Automated trading can lose money, and rules depend on your jurisdiction and the platform's terms. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.
Client order ID FIX tag: ClOrdID (11), OrigClOrdID (41), OrderID (37)
In FIX the client order ID is ClOrdID, tag 11; tag 41 points at the order being canceled or replaced, and tag 37 is the venue's own ID.
| Tag | Field | Assigned by | Rule |
|---|---|---|---|
| 11 | ClOrdID | Buy side | Uniqueness must be guaranteed within a single trading day; firms with multi-day orders should ensure it across days, for example by embedding a date |
| 41 | OrigClOrdID | Buy side | ClOrdID of the previous non-rejected order, not the first order of the day, when canceling or replacing |
| 37 | OrderID | Sell side | Identifier of the most recent order, assigned by the broker or exchange |
So each replace carries a fresh tag 11 and links back through tag 41 to the previous step, not to the original order. The standard puts the uniqueness duty on the sender and names the rejection, OrdRejReason (103) = 6, Duplicate Order; DuplicateClOrdIDIndicator (2829) marks an intentional duplicate. The rest is left to each venue. Kraken, for example, extends its uniqueness check across open orders and the FIX session, and requires FIX clients to send ever-increasing numbers or timestamp-first UUIDs. Session mechanics are in broker API integration.
Idempotent order creation: client order ID rules by venue
Idempotent order creation means a repeated request cannot create a second order, and only some venues document that behavior.
| Venue and field | Format | Documented uniqueness | Repeated ID | Retry after a fill |
|---|---|---|---|---|
Alpaca client_order_id | Up to 128 characters; generated if omitted | "A unique identifier"; an Alpaca article of 4 March 2026 says "each active order" | HTTP 422, code 40010001, client_order_id must be unique | Not stated |
Binance Spot newClientOrderId | ^[a-zA-Z0-9-_]{1,36}$ | Among open orders; accepted again once the previous order is filled | -2010 Duplicate order sent. | ID is free: second order |
Binance USD-M newClientOrderId | ^[\.A-Z\:/a-z0-9_-]{1,36}$ | Among open orders | -4116 clientOrderId is duplicated | ID is free: second order |
Bybit orderLinkId | Up to 36: letters, digits, dash, underscore | "Always unique" on futures, perps and spot; period not stated | 110072 (UTA), 170141 (spot) | Rejected, if "always" has no limit |
Coinbase Advanced Trade client_order_id | Required string | "A unique ID"; period not stated | No new order; the existing one is returned | Existing order returned |
FIX ClOrdID | String | Within a trading day | OrdRejReason (103) = 6, where the venue enforces it | Venue's rules |
Hyperliquid cloid | Optional 128-bit hex, such as 0x1234567890abcdef1234567890abcdef | Not stated | Not stated | Not stated |
Kraken Spot cl_ord_id | Long UUID, 32-hex short UUID, or ASCII text up to 18 characters | Open orders per client, plus the FIX session | Not stated in the guide | ID is free: second order |
OKX clOrdId | Up to 32 alphanumerics | Pending orders; reusable after filled or canceled | 51016 Client order ID already exists. | ID is free: second order |
Kraken's userref is a non-unique integer, so it cannot do this job. Hyperliquid has a different guard: its nonce page says a signer's nonce must never have been used before, so an identical signed message should not execute twice (a reading, not a test), and it warns that nonce state can be pruned after an API wallet is deregistered.
Binance Spot's wording leaves a gap: one sentence says unique among open orders, the next says accepted only when the previous one is filled, and the order-list endpoints say filled or completely expired. The page does not settle what happens after a plain cancel, so do not build on reuse.
Client order ID on Binance: a retry after a fill can create a second order
A check among open orders only rejects a retry while the first order is still open, so an order that filled during the timeout leaves nothing to collide with.
Hypothetical timeline. A bot sends a market buy of 0.5 with ID A, and the exchange fills it at once. The reply is lost, and the HTTP client times out. The bot resends with ID A, trusting the ID to protect it. Order A is no longer open, so the duplicate check passes. The position is now 1.0 instead of 0.5.
Binance's Spot documents say APIs time out after 10 seconds waiting for the matching engine, and an HTTP 5XX status means the outcome is unknown and could have been a success. The trap needs a signed order, so it was not run against the Spot Testnet; the last section gives the test to run with your own key.
How to find out what happened after a timeout
Ask by client order ID first, and treat a not-found answer as provisional, because every venue's lookup has a blind spot.
| Venue | Lookup | Blind spot |
|---|---|---|
| Alpaca | GET /v2/orders:by_client_order_id | Not stated |
| Binance Spot | GET /api/v3/order with origClientOrderId, weight 4 | The API system is asynchronous, with some delay normal |
| Binance USD-M | GET /fapi/v1/order with origClientOrderId, weight 1 | Canceled or expired orders without fills are not found after 3 days, any order after 90 |
| Bybit | orderLinkId on GET /v5/order/realtime | Latest 500 closed orders; after a server restart Unified accounts must use history, with the windows in position reconciliation |
| Coinbase | Repeat the create request with the same ID; Get Order marks its client_order_id parameter deprecated | How long IDs are remembered; a repeat creates the order if the first never arrived |
| Hyperliquid | POST /info, type orderStatus, with the cloid in oid | The testnet returned {"status":"unknownOid"} for an unused ID on 5 November 2026; a malformed ID got HTTP 422 |
| Kraken | cl_ord_id on open and closed orders | Query Orders Info marks txid as required, which a lost reply never gave you |
| OKX | GET /api/v5/trade/order with clOrdId | Returns only the latest match if an ID was reused |
In FIX the equivalent is OrderStatusRequest (35=H), and PossResend (97) flags a message that may have gone under another sequence number. Check the private stream too, wait out the venue's own timeout and query more than once. Only then resend, with the same ID rather than a new one: if the first order surfaces while still open, the venue rejects the repeat.
Designing IDs that survive a restart
Derive the ID from the intent and store it before sending, so a restarted process regenerates the same ID instead of inventing a new one.
- Format. 32 hex characters fit every format in the table above, with
0xadded for Hyperliquid. A dashed UUID fails OKX's alphanumeric rule, and Kraken caps free text at 18 characters, though its own example has 19. - Measured. On the Spot Testnet on 5 November 2026, a 32-hex ID and a 36-character UUID passed the format check and stopped at the key check with
-2015. A 37-character ID anda:breturned-1100, whose message gives the pattern quoted above, so USD-M's extra characters do not carry over to Spot. - Time. For FIX orders that live across days, add a date, and check the venue's rule first: a hash does not meet Kraken's FIX rule.
Tested Python: persist before send, query before retry
This sketch runs without a network: a synthetic venue accepts the order, loses the reply and fills it, while SQLite keeps each intent across a simulated crash.
import hashlib, re, sqlite3
def make_id(bot, intent_seq):
"""Same intent, same ID, even after a restart: 32 hex characters."""
return hashlib.sha256(f"{bot}:{intent_seq}".encode()).hexdigest()[:32]
FORMATS = { # rules read 5 November 2026
"Binance Spot": r"[a-zA-Z0-9_-]{1,36}",
"Binance USD-M": r"[.A-Z:/a-z0-9_-]{1,36}",
"Bybit": r"[A-Za-z0-9_-]{1,36}",
"Kraken short UUID": r"[0-9a-f]{32}",
"OKX": r"[A-Za-z0-9]{1,32}",
}
cid = make_id("bot1", 42)
print(cid, "same after restart:", cid == make_id("bot1", 42))
print("valid on all:", all(re.fullmatch(p, cid) for p in FORMATS.values()))
class Venue:
"""Synthetic. 'open' follows Binance Spot's text, 'always' Coinbase's."""
def __init__(self, scope):
self.scope, self.orders = scope, []
def place(self, cid, lose_reply=False):
same = [o for o in self.orders if o["cid"] == cid]
if same and self.scope == "always":
return same[-1] # nothing new is created
if any(o["status"] == "NEW" for o in same):
raise ValueError("Duplicate order sent.")
self.orders.append({"cid": cid, "status": "NEW"})
if lose_reply:
raise TimeoutError # accepted, reply lost
return self.orders[-1]
def query(self, cid):
return next((o for o in self.orders if o["cid"] == cid), None)
def fill_all(self):
for o in self.orders:
o["status"] = "FILLED"
def blind_retry(venue, cid):
try:
venue.place(cid, lose_reply=True)
except TimeoutError:
venue.fill_all() # it fills during the timeout
venue.place(cid) # same ID, no lookup first
def submit(db, venue, cid, crash=False):
db.execute("INSERT OR IGNORE INTO orders VALUES (?, 'INTENT')", (cid,))
db.commit() # durable before anything is sent
if crash:
return # the process dies here
try:
venue.place(cid, lose_reply=True)
except TimeoutError:
venue.fill_all()
db.execute("UPDATE orders SET state='UNKNOWN' WHERE cid=?", (cid,))
db.commit()
def recover(db, venue):
"""Run at startup and after every timeout: ask first, then decide."""
rows = db.execute("SELECT cid FROM orders WHERE state IN ('INTENT','UNKNOWN')")
for (cid,) in rows.fetchall():
try:
if venue.query(cid) is None: # wait out delays before trusting this
venue.place(cid)
except ValueError: # duplicate: the order exists
pass
except TimeoutError: # still unknown: retry next pass
continue
db.execute("UPDATE orders SET state='ACKED' WHERE cid=?", (cid,))
db.commit()
for scope in ("open", "always"):
v = Venue(scope)
blind_retry(v, cid)
print(f"blind retry, {scope} scope: {len(v.orders)} order(s)")
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (cid TEXT PRIMARY KEY, state TEXT)")
v = Venue("open")
submit(db, v, cid)
recover(db, v)
print(f"query first, open scope: {len(v.orders)} order(s)")
submit(db, v, make_id("bot1", 43), crash=True)
recover(db, v)
recover(db, v)
print(f"crash before the send, two restarts: {len(v.orders)} orders for 2 intents")Output on Python 3.11, run 5 November 2026:
36d244912785592080298e555758f161 same after restart: True
valid on all: True
blind retry, open scope: 2 order(s)
blind retry, always scope: 1 order(s)
query first, open scope: 1 order(s)
crash before the send, two restarts: 2 orders for 2 intentsThe two scopes mirror Binance Spot's and Coinbase's documented text; neither is a live result.
What the documents do not say, and how to test it
Several rules a retry depends on are not published; test them on the venue's test environment first.
- How long Coinbase and Bybit remember an ID.
- Whether Binance Spot accepts an ID again after a cancel or a rejection.
- What a repeated
cloidon Hyperliquid or a repeated ID on Kraken returns, and whether Alpaca frees an ID once an order ends.
For each venue, run three sequences with one ID: place, fill, repeat; place, cancel, repeat; place, reject, repeat. Record the response and the order count, and rerun after API changes. Retry policy in general is in crypto exchange API integration. For integration work in general, see exchange API integration; naming a venue here does not mean Fincode integrates it or has built on it.
Frequently asked questions
Can I reuse a client order ID?
Some venues allow it after an order ends, including Binance after a fill and OKX after a fill or cancel, but reuse makes lookups ambiguous. Treat every ID as single use.
How long can a Binance client order ID be?
Up to 36 characters on Spot and USD-M futures. Spot allows letters, digits, dash and underscore, and USD-M also allows dot, colon and slash, as read on 5 November 2026.
What format does a Hyperliquid cloid use?
An optional 128-bit hex string such as 0x1234567890abcdef1234567890abcdef. The orderStatus info request accepts it in place of the order ID.
Does a client order ID alone prevent duplicate orders?
No. It gives you something to look up. You still have to store it before sending, query after a timeout and only then decide whether to resend.
Sources
- Binance Spot API documentation: REST API (newClientOrderId, query order, timeouts, data sources) (opens in a new tab)
- Binance Spot API documentation: Error codes (Duplicate order sent.) (opens in a new tab)
- Binance USD-M Futures: Trade endpoints (newClientOrderId pattern, query order limits) (opens in a new tab)
- Binance USD-M Futures: Error codes (-4116) (opens in a new tab)
- Bybit V5 API: Place Order (orderLinkId) (opens in a new tab)
- Bybit V5 API: Error codes (110072, 170141) (opens in a new tab)
- Coinbase Advanced Trade API: Create Order (client_order_id) (opens in a new tab)
- Kraken Developers: Client order identifiers (cl_ord_id, userref, FIX) (opens in a new tab)
- Hyperliquid Docs: Info endpoint (order status by oid or cloid) (opens in a new tab)
- Hyperliquid Docs: Nonces and API wallets (opens in a new tab)
- OKX API v5: Place order (clOrdId rules) (opens in a new tab)
- Alpaca Trading API: Create an Order (client_order_id) (opens in a new tab)
- Alpaca Learn: How to fix common Trading API errors, 4 March 2026 (HTTP 422 on a duplicate ID) (opens in a new tab)
- FIX Trading Community, FIXimate: ClOrdID (11), FIX Latest (opens in a new tab)
- FIX Trading Community, FIXimate: DuplicateClOrdIDIndicator (2829), with OrdRejReason 6, FIX Latest (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.