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
- Ensure TPS is resolved through `resolve_tps` (which substitutes the default for 0) before calling `window_limit`.
- Verify `DERIVE_RATE_BURST_MULTIPLIER` is non-zero in your build.
- 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
- Always resolve TPS through resolve_tps so zero falls back to the default.
- Keep DERIVE_RATE_BURST_MULTIPLIER non-zero.
- Don't construct the limiter with raw user config without the resolve step.
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
- Invalid factory address for DEX {name} on chain {chain} for
- non-zero
- non-zero
- checked above
- window index fits u32
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/ab706b7b8269a286.
Report an issue: GitHub.