Skip to main content

Order Concurrency & Nonces

This page explains how to submit many orders concurrently from a single account - the capability quote engines and HFT desks need - and how the windowed nonce model makes it safe. If you have integrated against an exchange that forces strictly sequential nonces, read this first: Senticore does not work that way, and assuming it does will needlessly serialize your flow.

The problem with sequential nonces

Most signed-order venues require each order's nonce to be exactly previous + 1. That turns one account into a single-file queue: order N+1 cannot be accepted until order N has been acknowledged. For a market maker running tens or hundreds of in-flight quotes per account, that head-of-line blocking caps throughput at one round-trip per order and makes a single slow ack stall the whole strategy.

The solution: a windowed nonce

Each account has a nonce window of 256. A signed action's nonce is accepted if it is:

  • inside the open window [nonceFloor, nonceFloor + 256), and
  • not already used or in flight.

Gaps and out-of-order submission are allowed. You do not have to wait for an ack before sending the next nonce, and you do not have to reserve a range. This means an account can have up to 256 orders in flight at once, submitted concurrently across as many parallel connections as you like.

The floor advances automatically: as contiguous nonces at the bottom of the window are consumed, nonceFloor slides up and the window opens further ahead. So a steadily-incrementing counter keeps the window moving with you.

Concurrency is per account

The window is per account, not per connection. Two connections (or an HTTP client plus a FIX session) sharing one account share one window. Coordinate your nonce counter across them, or shard strategies across accounts.

Nonce is replay protection, not execution priority

A lower nonce does not get matched first. Execution order is decided by the order in which signed actions arrive at the sequencer, not by nonce value. The nonce only protects against replay and duplicates. Send nonce 4810 and 4811 concurrently and whichever the sequencer ingests first is sequenced first - design your strategy around arrival order, not nonce order.

How to pipeline orders

  1. Keep a single monotonically increasing nonce counter per account (start it at nonceFloor from any recent response, or from GET /api/v1/public/time-style bootstrap plus a nonce-floor read).
  2. For each order, take the next counter value, sign, and fire immediately - do not block on the previous ack.
  3. Keep your in-flight depth at or below the window size (256). If you routinely need more than 256 simultaneously-open nonces on one account, spread the strategy across multiple accounts.
  4. Reconcile fills and order state from the private execution stream or a full-mode response, not from nonce order.
// Fire 50 quotes concurrently from one account - no waiting between submits.
const base = nonceFloor; // from a recent response
const orders = buildQuotes(); // your 50 price/size pairs

await Promise.all(
orders.map((order, i) => {
const payload = {
account,
nonce: base + i, // unique, in-window, out-of-order OK
ts: Date.now(),
action: order,
};
return client.trading.submitSignedAction(
signAction(payload, key),
{ idempotencyKey: `quote-${base + i}` },
);
}),
);

You may submit these on one connection or many; the window does not care. The admission precheck explicitly ignores an action's own already-claimed pending nonce, so a burst like the one above does not falsely reject itself - while a genuinely duplicate (account, nonce) is still rejected.

Recovering from a nonce reject

Every nonce rejection is structured and carries the window so you can resync without guessing. The response includes nonceFloor, nonceWindow (256), nextUsableNonce (a convenience hint equal to nonceFloor), and a stable code:

{
"accepted": false,
"code": "nonce_below_floor",
"error": "nonce 4600 is below replay window; nonceFloor=4810 nonceWindow=256",
"nextUsableNonce": 4810,
"nonceFloor": 4810,
"nonceWindow": 256
}
CodeRetryableWhat to do
nonce_below_floorNo - pick freshThe nonce is permanently stale/consumed. Advance your counter to at least nonceFloor and pick an unused in-window value.
nonce_outside_windowAfter the floor advancesYou ran too far ahead. Fill the lower in-window nonces first; the floor advances and the window opens.
nonce_replayedNo - pick freshThat nonce is already used or in flight. Pick a different unused in-window value.
nonce_mismatchAfter resyncLegacy/uncategorized reject. Resync from nonceFloor / nextUsableNonce.

Practical rule: on any nonce reject, snap your local counter to max(localCounter, nonceFloor), drop the rejected nonce, and continue. You never need to halt the whole account for one stale nonce.

What you do not need

  • No nonce reservation. The old nonceReservationId flow is deprecated; omit it. The signing bytes and v1 domain are unchanged.
  • No ack-then-next loop. Do not serialize on acknowledgements.
  • No strict +1 sequence. Gaps are fine; the floor slides over them as they fill.

Note on transport vs action nonces

The windowed nonce described here is the nonce inside the signed ActionPayload - it governs order replay protection and concurrency. It is distinct from the per-request SC-Nonce used on the HMAC API-agent transport, which is a monotonic authentication nonce for the request envelope. The concurrency guarantees on this page are about the action-payload nonce.