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 found a position snapshot whose replay_state JSON value failed serde_json deserialization into the expected replay state type. This error means the persisted snapshot is present but its embedded replay state does not match the expected structure.

Source

Thrown at crates/infrastructure/src/sql/queries.rs:942

        .map(|rows| rows.into_iter().map(|row| row.0).collect())
        .map_err(|e| anyhow::anyhow!("Failed to load position events: {e}"))
    }

    /// Loads and replays a complete `Position` for a `position_id` via the provided `pool`.
    ///
    /// # Errors
    ///
    /// Returns an error if loading events, loading instruments, or replaying fills fails.
    pub async fn load_position(
        pool: &PgPool,
        position_id: &PositionId,
    ) -> anyhow::Result<Option<Position>> {
        if let Some(snapshot) = Self::load_position_snapshot(pool, position_id).await?
            && 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 fills = Self::load_position_events(pool, position_id).await?;
        let Some((first_fill, remaining_fills)) = fills.split_first() else {
            return Ok(None);
        };
        let Some(instrument) = Self::load_instrument(pool, &first_fill.instrument_id).await? else {
            log::error!(
                "Instrument not found for position {position_id}: {}",
                first_fill.instrument_id
            );
            return Ok(None);
        };

        let mut position = Position::new(&instrument, first_fill.clone());
        for fill in remaining_fills {
            if position.trade_ids().contains(&fill.trade_id) {
                anyhow::bail!(

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the wrapped serde_json error to see which field/type failed.
  2. Compare the stored replay_state JSON against the current Rust struct and migrate old snapshots.
  3. If snapshots are optional for you, clear the snapshot row so load_position falls back to replaying raw fill events via load_position_events.
  4. Ensure writes and reads use the same nautilus version, or run the provided data migrations.

Example fix

// before
let position = DatabaseQuery::load_position(&pool, &position_id).await?;
// after
let position = match DatabaseQuery::load_position(&pool, &position_id).await {
    Ok(p) => p,
    Err(e) if e.to_string().contains("Failed to decode position replay state") => {
        tracing::warn!("Snapshot unusable for {position_id}, rebuilding from events: {e:#}");
        None // fall back to event replay / re-snapshot
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: fallback

Try / catch

match load_position(&pool, &pid).await {
    Ok(p) => p,
    Err(e) if e.to_string().contains("Failed to decode position replay state") => {
        rebuild_from_events(&pool, &pid).await.ok()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling load_position(pool, position_id) where load_position_snapshot returns a snapshot with a replay_state that serde_json cannot deserialize — wrong shape, missing fields, or type changes between versions.

Common situations: Snapshot written by an older nautilus version whose Position replay state schema changed; manually edited snapshot rows; truncated/corrupted JSON stored in the snapshot column.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


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