#314 Aug 12, 2026

314. Duration::mul_f64 — timeout * 1.5 Doesn't Compile, So You Round-Tripped Through Floats

Duration * 2 compiles, Duration * 1.5 doesn’t — and the usual workaround tears the duration apart into an f64 just to build it back up again.

Duration implements Mul<u32>, so doubling a timeout is easy. But backoff factors are rarely whole numbers — the classic multiplier is 1.5 or 2.0-with-jitter, and that’s where the type checker stops you:

1
2
3
4
5
6
7
use std::time::Duration;

let delay = Duration::from_millis(200);

let doubled = delay * 2;        // fine: Mul<u32>
// let next = delay * 1.5;      // error: cannot multiply
//                              // `Duration` by `{float}`

The workaround everyone reaches for is the float round-trip:

1
let next = Duration::from_secs_f64(delay.as_secs_f64() * 1.5);

It works, but it’s noisy, and it funnels a perfectly good Duration through a lossy f64 in both directions.

Multiply the duration directly

mul_f64 (and div_f64) have been on Duration since Rust 1.38 — they do the scaling in one call and keep nanosecond precision:

1
2
3
4
5
6
use std::time::Duration;

let delay = Duration::from_millis(200);

assert_eq!(delay.mul_f64(1.5), Duration::from_millis(300));
assert_eq!(delay.div_f64(2.0), Duration::from_millis(100));

Which makes an exponential backoff loop read exactly like the algorithm:

1
2
3
4
5
6
let mut delay = Duration::from_millis(100);
for _ in 0..4 {
    delay = delay.mul_f64(1.5).min(Duration::from_secs(2));
}
// 100ms → 150ms → 225ms → 337.5ms → 506.25ms
assert_eq!(delay, Duration::new(0, 506_250_000));

One caveat carried over from float land: mul_f64 panics if the factor produces a negative, non-finite, or overflowing result — the same failure mode as from_secs_f64 in bite 311. If your factor comes from untrusted math (jitter that can dip negative), clamp it first or go through Duration::try_from_secs_f64.

That closes out the week’s Duration tour: 311 built one from a float safely, 312 subtracted without panicking, 313 divided two of them — and today’s bite scales one without leaving the type.

← Previous 313. div_duration_f64 — Your Progress Bar Sat at 0% Because as_secs() Truncated First Next → 315. SystemTime::duration_since — NTP Set the Clock Back, and elapsed().unwrap() Panicked