nautechsystems/nautilus_trader · error

Duplicate fill event for position {position_id}: {trade_id}

Error message

Duplicate fill event for position {position_id}: {trade_id}

What it means

While rebuilding a Position from cached fill (order filled) events, the loader detected a trade_id that the position has already applied. Applying the same fill twice would corrupt position state (double-counted quantity/realized PnL), so the loader bails instead. It indicates corrupted or duplicated cache data for that position.

Source

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

            .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?
        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!(
                    "Duplicate fill event for position {position_id}: {}",
                    fill.trade_id
                );
            }
            position.apply(fill);
        }

        Ok(Some(position))
    }

    fn get_collection_key(key: &str) -> anyhow::Result<&str> {
        key.split_once(REDIS_DELIMITER)
            .map(|(collection, _)| collection)
            .ok_or_else(|| {
                anyhow::anyhow!("Invalid `key`, missing a '{REDIS_DELIMITER}' delimiter, was {key}")
            })
    }

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the cached fills list for the position in Redis and remove the duplicate trade_id entries
  2. Verify only one trader instance/process is writing to that cache database (check instance_id / trader_id separation)
  3. Clear and rebuild the cache for that trader from the event stream so fills are persisted exactly once
  4. If this recurs, add a dedup guard before persisting fills to the cache

Example fix

// before
// duplicate fills persisted: LOADS fill twice, load_position bails
// after
if !position.trade_ids().contains(&fill.trade_id) {
    cache.add_order_fill(&fill)?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before load, scan cached fills for duplicate trade_ids
let ids: HashSet<&str> = fills.iter().map(|f| f.trade_id.as_str()).collect();
if ids.len() != fills.len() { /* duplicates present — rebuild cache */ }

Try / catch

match cache.load_position(&position_id) {
    Ok(p) => p,
    Err(e) if e.to_string().contains("Duplicate fill event") => {
        log::error!("cache corrupted for {position_id}; rebuilding from events");
        rebuild_position_from_events(&position_id)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling load_position for a position whose cached fill events contain the same trade_id more than once — e.g. duplicate writes to the Redis list, a snapshot restored over live data, or replaying events into an already-built position.

Common situations: Redis cache keys shared by two trader instances writing the same fills; a crash mid-write causing partial duplicate entries; manually copying/merging cache snapshots and duplicating fill entries.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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