nautechsystems/nautilus_trader · error

Lighter connection or nonce state changed during transaction

Error message

Lighter connection or nonce state changed during transaction preparation

What it means

A TOCTOU guard in build_tx_context: after acquiring the nonce submission gate, the code re-checks that the connection epoch and nonce-ready epoch still match those observed at entry. If the WS reconnected or a nonce refresh began mid-preparation, the reserved context would be built on stale nonce state, so preparation is aborted.

Source

Thrown at crates/adapters/lighter/src/execution.rs:1470

    }

    // Per-order `params["market_order_slippage_bps"]` overrides the config default.
    fn resolve_slippage_bps(&self, params: Option<&Params>) -> u32 {
        params
            .and_then(|p| p.get_u64("market_order_slippage_bps"))
            .map_or(self.config.market_order_slippage_bps, |v| v as u32)
    }

    fn build_tx_context(&self, credential: &Credential) -> anyhow::Result<ReservedTxContext> {
        let connection_epoch = self.ws_client.connection_epoch();
        anyhow::ensure!(
            self.nonce_ready_connection_epoch.load(Ordering::Acquire) == connection_epoch,
            "Lighter nonce refresh is pending for connection epoch {connection_epoch}",
        );
        let nonce_guard = Arc::clone(&self.nonce_submission_gate)
            .try_read_owned()
            .context("Lighter nonce refresh is in progress")?;
        anyhow::ensure!(
            self.ws_client.connection_epoch() == connection_epoch
                && self.nonce_ready_connection_epoch.load(Ordering::Acquire) == connection_epoch,
            "Lighter connection or nonce state changed during transaction preparation",
        );
        let nonce = match self
            .dispatch
            .nonce_manager
            .next_nonce(credential.account_index(), credential.api_key_index())
        {
            Ok(nonce) => nonce,
            Err(e @ NonceError::SkipWindowExhausted { .. }) => {
                // Lost acks leave the baseline stale; resync from the venue so
                // later commands recover. The fetch is async; this command fails.
                self.spawn_nonce_window_recovery(credential);
                anyhow::bail!("failed to allocate Lighter nonce: {e}");
            }
            Err(e) => anyhow::bail!("failed to allocate Lighter nonce: {e}"),
        };

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Retry the operation from scratch — the next attempt will run against the settled connection state.
  2. Pause command dispatch during reconnects and resume only after readiness is re-signaled.
  3. Avoid issuing signed txs from multiple tasks concurrently at reconnect boundaries.
  4. If persistent, investigate WS stability (keepalives, proxy timeouts).

Example fix

// before
let ctx = execution.prepare_signed_modify_order(&cmd, &credential)?;
// after
let ctx = loop {
    match execution.prepare_signed_modify_order(&cmd, &credential) {
        Ok(ctx) => break ctx,
        Err(e) if e.to_string().contains("state changed") => {
            tokio::time::sleep(Duration::from_millis(100)).await;
        }
        Err(e) => return Err(e),
    }
};
Defensive patterns

Strategy: retry

Try / catch

match result {
    Err(e) if e.to_string().contains("state changed during transaction preparation") => retry_with_backoff(),
    other => other,
}

Prevention

When it happens

Trigger: Concurrent reconnect/nonce refresh racing with any of update_leverage, prepare_signed_modify_order, or prepare_integrator_auto_approval: the epoch or nonce_ready_connection_epoch changed between the first check and gate acquisition.

Common situations: High-frequency order management during unstable network; multiple tasks issuing signed commands at reconnect time; automated loops calling modify/leverage APIs continuously across reconnect boundaries.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/44df320a7a9fb9a4. Report an issue: GitHub.