nautechsystems/nautilus_trader · error

Failed to decode position replay state: {e}

Error message

Failed to decode position replay state: {e}

What it means

load_position scans position snapshots in reverse order looking for a replay_state; when found it converts the stored JSON Value into the replay-state type with serde_json::from_value. This error is thrown when that JSON value's shape does not match the expected replay state struct — i.e. the snapshot's embedded replay state is malformed or schema-drifted.

Source

Thrown at crates/infrastructure/src/redis/queries.rs:924

    ///
    /// # Errors
    ///
    /// Returns an error if the underlying read or deserialization fails.
    pub async fn load_position(
        con: &ConnectionManager,
        trader_key: &str,
        position_id: &PositionId,
        encoding: SerializationEncoding,
    ) -> anyhow::Result<Option<Position>> {
        let snapshot_key =
            format!("{SNAPSHOTS}{REDIS_DELIMITER}{POSITIONS}{REDIS_DELIMITER}{position_id}");
        let snapshots = Self::read(con, trader_key, &snapshot_key).await?;
        for payload in snapshots.iter().rev() {
            let snapshot: PositionSnapshot = Self::deserialize_payload(encoding, payload)?;
            if let Some(replay_state) = snapshot.replay_state {
                return serde_json::from_value(replay_state)
                    .map(Some)
                    .map_err(|e| anyhow::anyhow!("Failed to decode position replay state: {e}"));
            }
        }

        let key = format!("{POSITIONS}{REDIS_DELIMITER}{position_id}");
        let result = Self::read(con, trader_key, &key).await?;
        if result.is_empty() {
            return Ok(None);
        }

        let fills: Vec<OrderFilled> = result
            .iter()
            .map(|payload| Self::deserialize_payload(encoding, payload))
            .collect::<anyhow::Result<_>>()?;
        let Some((first_fill, remaining_fills)) = fills.split_first() else {
            return Ok(None);
        };
        let Some(instrument) =
            Self::load_instrument(con, trader_key, &first_fill.instrument_id, encoding).await?

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the serde field path in {e} and fix that field in the stored snapshot.
  2. Delete the offending snapshot so load_position falls back to the position events stream and rebuilds state.
  3. Re-snapshot positions from a fresh session with the current version.
  4. Align reader/writer versions so replay-state schemas match.

Example fix

// before: old snapshot replay_state lacks required field
{"replay_state": {"last_event_id": 5}}
// after: provide all required fields for the current schema
{"replay_state": {"last_event_id": 5, "ts_last": 1704067200000000000}}
Defensive patterns

Strategy: validation

Validate before calling

// Rust: pre-validate replay_state JSON against expected struct
fn replay_state_valid(v: &serde_json::Value) -> bool {
    serde_json::from_value::<ReplayState>(v.clone()).is_ok()
}

Type guard

fn has_replay_state(snapshot: &PositionSnapshot) -> bool {
    snapshot.replay_state.is_some()
}

Try / catch

match load_position(&mut con, trader_key, position_id, encoding).await {
    Ok(pos) => pos,
    Err(e) if e.to_string().contains("replay state") => {
        // drop stale snapshot and rebuild state from position events
        delete_stale_snapshot(&mut con, trader_key, position_id).await?;
        load_position(&mut con, trader_key, position_id, encoding).await.ok()
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling load_position when a PositionSnapshot in Redis contains a Some(replay_state) JSON object missing required fields or with wrong field types for the current ReplayState/position-state struct version.

Common situations: Snapshots written by an older NautilusTrader version and read by a newer one; hand-edited snapshot payloads; partially written snapshot records after a crash mid-write.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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