clockworklabs/SpacetimeDB · error

Duration overflows i64 microseconds

Error message

Duration overflows i64 microseconds

What it means

Panic in `TimeDuration::from_duration`: converting a `std::time::Duration` to SpacetimeDB's microsecond-resolution `TimeDuration` requires `duration.as_micros()` to fit in an `i64` (max ≈ 292,471 years). The `try_into().expect("Duration overflows i64 microseconds")` panics for larger spans. The doc comment explicitly documents this as a panic condition.

Solutions

  1. Find the caller: debug-panic or log `duration` at the call site — the value will be astronomically large, revealing the arithmetic bug.
  2. Use `Duration::saturating_mul`/`saturating_add` (or `checked_*` + fallback) when computing durations from exponents or sums.
  3. If input can be arbitrary (config files, user strings), clamp to a sane maximum (e.g. 24h/365d) before conversion.
  4. Add unit tests with extreme inputs for duration math paths that feed `TimeDuration`.

Example fix

// before: unbounded backoff growth can overflow i64 micros
let delay = base * 2u32.pow(attempts as u32);
let td = TimeDuration::from_duration(delay);

// after: saturate the computation
let delay = base.saturating_mul(2u32.saturating_pow(attempts as u32)).min(Duration::from_secs(86_400));
let td = TimeDuration::from_duration(delay);
Defensive patterns

Strategy: validation

Validate before calling

const MAX_TD: Duration = Duration::from_secs(60 * 60 * 24 * 365 * 100_000);
fn checked_from_duration(d: Duration) -> Result<TimeDuration, Overflow> {
    (d <= MAX_TD).then(|| TimeDuration::from_duration(d)).ok_or(Overflow)
}

Prevention

When it happens

Trigger: Calling `TimeDuration::from_duration(d)` (or APIs that route through it) with a Duration produced by overflowing arithmetic — e.g. multiplying durations, `Duration::MAX` scaling, or a Duration built from summed intervals in timers/schedulers. Realistic wall-clock durations never exceed ~292k years; this is almost always a computation bug or unvalidated external input.

Common situations: Retry/backoff calculators multiplying delays (e.g. `base * 2^64` without saturation); parsing user/config-supplied time strings with absurd units (typos like '9999999 days'); accumulator loops summing durations without bounds; fuzzing with arbitrary Duration inputs.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/ed2915bb16133d00. Report an issue: GitHub.

Appendix: source

Thrown at crates/sats/src/time_duration.rs:78

    /// Converts `self` to `Duration`, clamping to 0 if negative.
    pub fn to_duration_saturating(self) -> Duration {
        self.to_duration().unwrap_or(Duration::ZERO)
    }

    /// Returns a positive `TimeDuration` with the magnitude of `self`.
    pub fn abs(self) -> Self {
        Self::from_micros(self.to_micros().saturating_abs())
    }

    /// Return a [`TimeDuration`] which represents the same span as `duration`.
    ///
    /// Panics if `duration.as_micros` overflows an `i64`
    pub fn from_duration(duration: Duration) -> Self {
        Self::from_micros(
            duration
                .as_micros()
                .try_into()
                .expect("Duration overflows i64 microseconds"),
        )
    }

    /// Returns `Some(self + other)`, or `None` if that value would be out of bounds for [`TimeDuration`].
    pub fn checked_add(self, other: Self) -> Option<Self> {
        self.to_micros().checked_add(other.to_micros()).map(Self::from_micros)
    }

    /// Returns `Some(self - other)`, or `None` if that value would be out of bounds for [`TimeDuration`].
    pub fn checked_sub(self, other: Self) -> Option<Self> {
        self.to_micros().checked_sub(other.to_micros()).map(Self::from_micros)
    }

    /// Generate an `iso8601` format string.
    ///
    /// This is the better supported format for use for the `pg wire protocol`.
    ///
    /// Example:

View on GitHub (pinned to 6dee26c6ef)