nautechsystems/nautilus_trader · error

window limit must be non-zero

Error message

window limit must be non-zero

What it means

`window_limit` builds the rate-limiter's burst window limit as `tps * DERIVE_RATE_BURST_MULTIPLIER` and converts to `NonZeroU32`. The panic fires only if the product is zero — `saturating_mul` overflow clamps to u32::MAX (non-zero), so zero requires tps == 0 AND the multiplier == 0, or a zero multiplier constant. It enforces that the limiter never gets a zero-capacity window.

Source

Thrown at crates/adapters/derive/src/common/rate_limit.rs:427

        RateBucket::PerInstrument(instrument_name) => Ustr::from(
            format!(
                "{DERIVE_PER_INSTRUMENT_RATE_KEY_PREFIX}{}",
                instrument_name.as_str(),
            )
            .as_str(),
        ),
        RateBucket::CancelAll => Ustr::from(DERIVE_CANCEL_ALL_RATE_KEY),
        RateBucket::CancelByLabel => Ustr::from(DERIVE_CANCEL_BY_LABEL_RATE_KEY),
    }
}

fn resolve_tps(configured: Option<u32>, default_tps: u32) -> u32 {
    configured.filter(|&v| v > 0).unwrap_or(default_tps)
}

fn window_limit(tps: u32) -> NonZeroU32 {
    NonZeroU32::new(tps.saturating_mul(DERIVE_RATE_BURST_MULTIPLIER))
        .expect("window limit must be non-zero")
}

fn window_index(elapsed_nanos: u64) -> u32 {
    u32::try_from(elapsed_nanos / RATE_WINDOW_NANOS).expect("window index fits u32")
}

/// Packs `(window index, consumed)` into one atomic word; the window index in
/// the high half so the default zero value reads as a stale window.
fn pack(window: u32, consumed: u32) -> u64 {
    (u64::from(window) << 32) | u64::from(consumed)
}

fn unpack(packed: u64) -> (u32, u32) {
    (
        u32::try_from(packed >> 32).expect("window index fits u32"),
        packed as u32,
    )
}

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure TPS is resolved through `resolve_tps` (which substitutes the default for 0) before calling `window_limit`.
  2. Verify `DERIVE_RATE_BURST_MULTIPLIER` is non-zero in your build.
  3. If constructing the limiter manually, pass a positive tps value.

Example fix

// before
let limiter = RateLimiter::new(window_limit(configured_tps));
// after
let tps = resolve_tps(configured_tps, DEFAULT_TPS);
let limiter = RateLimiter::new(window_limit(tps));
Defensive patterns

Strategy: validation

Validate before calling

// before constructing the limiter
let tps = configured.unwrap_or(DEFAULT_TPS);
assert!(tps > 0, "tps must be positive");

Type guard

fn tps_ok(tps: u32) -> bool { tps > 0 }

Prevention

When it happens

Trigger: Constructing the rate limiter with a configured/default TPS of 0 while the burst multiplier is also 0 — practically only from a mis-edited constant or a `resolve_tps` regression, since `resolve_tps` already filters non-positive values to a default.

Common situations: Hand-crafted configuration bypassing `resolve_tps`, e.g. constructing the limiter directly with `window_limit(0)` in tests or a fork that changed `DERIVE_RATE_BURST_MULTIPLIER`.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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