#315 Aug 13, 2026

315. SystemTime::duration_since — NTP Set the Clock Back, and elapsed().unwrap() Panicked

You timed a job with SystemTime and it ran fine for weeks — until NTP stepped the clock backwards and that innocent .elapsed().unwrap() took the service down.

SystemTime is the wall clock. It can jump backwards at any moment: NTP corrections, a manual clock change, a VM resuming from snapshot. That’s why every subtraction on it returns a Result — the “later” time can end up earlier:

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

let start = SystemTime::now();
do_work();
let taken = start.elapsed().unwrap(); // 💣 panics if
                                      // the clock stepped back

The Err isn’t noise to unwrap away — it’s the whole point of the API. It even tells you how far backwards the clock went:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::time::{Duration, SystemTime};

let earlier = SystemTime::now();
let later = earlier + Duration::from_secs(5);

// forward: Ok(gap)
assert_eq!(
    later.duration_since(earlier).unwrap(),
    Duration::from_secs(5)
);

// backwards: Err, carrying the gap
let err = earlier.duration_since(later).unwrap_err();
assert_eq!(err.duration(), Duration::from_secs(5));

Measuring time? Use Instant

If you’re timing how long something took, you don’t want the wall clock at all. Instant is the monotonic clock — the OS guarantees it never goes backwards, so there’s no Result to handle:

1
2
3
4
5
use std::time::Instant;

let start = Instant::now();
do_work();
let taken = start.elapsed(); // Duration, always valid

The rule of thumb

Instant for durations, SystemTime for timestamps. The one thing Instant can’t do is tell you when — it’s opaque, with no relation to any calendar. When you need an actual timestamp (log lines, cache expiry dates, file metadata), that’s SystemTime’s job:

1
2
3
4
5
6
use std::time::{SystemTime, UNIX_EPOCH};

let unix_secs = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .expect("clock before 1970")
    .as_secs();

And if a backwards step is survivable in your context, unwrap_or_default() turns it into a zero duration instead of a panic — the same “clamp at zero” move as saturating_sub in bite 312.

← Previous 314. Duration::mul_f64 — timeout * 1.5 Doesn't Compile, So You Round-Tripped Through Floats Next → 316. checked_duration_since — Your Deadline Passed, and Instant Subtraction Said Zero