nautechsystems/nautilus_trader · error

Invalid instrument key '{key}'

Error message

Invalid instrument key '{key}'

What it means

parse_instrument_key strips the configured prefix from a Redis instrument key and requires a non-empty remainder; otherwise it cannot recover the InstrumentId and throws 'Invalid instrument key'. This happens before InstrumentId parsing, so the key either lacks the prefix or is empty after stripping.

Source

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

    }

    async fn read_hset(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
        let result: HashMap<String, String> = conn.hgetall(key).await?;
        let json = serde_json::to_string(&result)?;
        Ok(vec![Bytes::from(json.into_bytes())])
    }

    async fn read_list(conn: &mut ConnectionManager, key: &str) -> anyhow::Result<Vec<Bytes>> {
        let result: Vec<Bytes> = conn.lrange(key, 0, -1).await?;
        Ok(result)
    }
}

fn parse_instrument_key(key: &str, prefix: &str) -> anyhow::Result<InstrumentId> {
    let value = key
        .strip_prefix(prefix)
        .filter(|value| !value.is_empty())
        .ok_or_else(|| anyhow::anyhow!("Invalid instrument key '{key}'"))?;
    InstrumentId::from_str(value)
        .map_err(|e| anyhow::anyhow!("Failed to parse instrument ID from key '{key}': {e}"))
}

fn is_timestamp_field(key: &str) -> bool {
    let expire_match = key == "expire_time_ns";
    let ts_match = key.starts_with("ts_");
    expire_match || ts_match
}

fn convert_timestamps(value: &mut Value) {
    match value {
        Value::Object(map) => {
            for (key, v) in map {
                if is_timestamp_field(key)
                    && let Value::Number(n) = v
                    && let Some(n) = n.as_u64()
                {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Set the Redis key_prefix config to exactly match the prefix used when the cache was written.
  2. Use a dedicated Redis database (or separate DB index) so scan patterns do not match foreign keys.
  3. Delete non-conforming keys or move them out of the scanned pattern.
  4. Log/skip bad keys in custom tooling before handing them to the loader.

Example fix

// before: stored under "myapp:instruments:...", reader uses default prefix
let config = RedisCacheConfig { key_prefix: "nautilus".into(), .. };
// after
let config = RedisCacheConfig { key_prefix: "myapp".into(), .. };
Defensive patterns

Strategy: validation

Validate before calling

// Rust: check key prefix before loading
fn has_prefix(key: &str, prefix: &str) -> bool {
    key.strip_prefix(prefix).map(|r| !r.is_empty()).unwrap_or(false)
}

Try / catch

match load_instruments(&mut con, trader_key, encoding).await {
    Ok(instruments) => instruments,
    Err(e) if e.to_string().contains("Invalid instrument key") => {
        tracing::warn!("non-conforming keys found: {e}; check key_prefix config");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: load_instruments or load_instrument_closes (via load_all) iterating keys where one does not start with the expected prefix (e.g. prefix config mismatch, foreign keys matched by the scan pattern, or an empty key).

Common situations: The Redis key_prefix config differs from the one used when writing; a shared Redis database contains unrelated keys matching the scan pattern; keys written by another tool without the prefix; empty string keys.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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