Working with Time

Time in programming is deceptively treacherous: clocks jump when NTP syncs, wall-clock time runs backwards across a daylight-saving boundary, and "how long did this take?" and "what time is it?" are answered by different clocks that must not be confused. Rust's std::time module makes the distinction explicit with two separate types, Instant and SystemTime, and picking the wrong one is the root of a whole class of subtle bugs. This tutorial covers which clock to use for what, Duration arithmetic that won't panic, and where the standard library ends and chrono begins.


Two Clocks: Instant vs SystemTime

The single most important distinction is that Rust has two clocks for two different questions:

  • Instant is a monotonic clock: it only ever moves forward, at a steady rate, and has no relationship to calendar time. It answers "how much time elapsed?"
  • SystemTime is the wall clock: the actual date and time, which can jump forward or backward when the system clock is adjusted. It answers "what time is it right now?"
use std::time::{Instant, SystemTime};

// measuring a duration → Instant (monotonic, immune to clock adjustments)
let start = Instant::now();
do_work();
let elapsed = start.elapsed();          // always a sensible positive Duration

// recording a timestamp / calendar time → SystemTime
let now = SystemTime::now();            // "when" something happened, in wall-clock terms

Using the right one is a correctness issue, not a style preference: if you measure elapsed time with SystemTime and the clock happens to sync backward mid-measurement, you can get a negative or wildly wrong duration. Instant cannot do that, which is exactly why it's the tool for benchmarks, timeouts, and rate limiting (Performance and Avoiding Allocations used it for timing).

Gotcha: never use SystemTime to measure how long something took. Because the wall clock can move backward, SystemTime::now() minus an earlier SystemTime can yield a negative interval, which is why the subtraction returns a Result (duration_since gives Err if time went backwards), not a bare Duration. For any "elapsed" measurement, use Instant, which is monotonic by construction and can't produce this bug. Reserve SystemTime for timestamps you'll display, store, or compare to calendar time.


Duration: Spans of Time

A Duration is a span (not a point), and it's what you get from subtracting instants or construct directly. It's precise to nanoseconds and always non-negative:

use std::time::Duration;

let timeout = Duration::from_secs(30);
let tick = Duration::from_millis(250);
let precise = Duration::from_nanos(500);

let total = timeout + tick;               // Durations add
let elapsed = Instant::now() - earlier;   // Instant - Instant = Duration

Duration is the currency of every time-related API: thread::sleep(Duration), tokio's timeout(Duration, ...) from Async Channels and select!, channel recv_timeout, they all speak Duration. Construct with the from_* helpers, and read back with .as_secs(), .as_millis(), .as_secs_f64() (the last for fractional seconds when you want, say, "2.5 seconds" as a float).


Arithmetic That Doesn't Panic

Time arithmetic can overflow or underflow, adding a huge Duration to an Instant, or subtracting a larger duration from a smaller one. The plain operators (+, -) panic on overflow, which is a landmine in code handling untrusted or computed durations. The checked_* and saturating_* variants make the edge case explicit:

let a = Duration::from_secs(5);
let b = Duration::from_secs(10);

// panics: 5s - 10s underflows (Duration can't be negative)
// let d = a - b;

// saturating: clamps to zero instead of panicking
let d = a.saturating_sub(b);              // Duration::ZERO

// checked: returns Option, None on overflow/underflow
let maybe = a.checked_sub(b);             // None

// Instant/SystemTime have checked_add/checked_sub too
let deadline = Instant::now().checked_add(Duration::from_secs(60));

Gotcha: subtracting Durations (or instants) with plain - panics on underflow, and it's easy to hit when one side is computed from input, deadline - now panics the moment now passes the deadline. Use saturating_sub when "clamp to zero" is the right behavior (a countdown that shouldn't go negative), or checked_sub when you need to detect the crossing. Reserve bare - for cases where you can prove the ordering. This is the same panic-vs-handle judgment as slice indexing (Slices, Chunks, and Windows).


Where std::time Ends and chrono Begins

std::time deliberately does not handle calendar concerns: it has no notion of years, months, time zones, formatting, or parsing. SystemTime is just an opaque point you can compare or convert to a Unix timestamp, there's no .year() or .format("%Y-%m-%d"). The moment you need human calendar operations, reach for the chrono crate:

// std: how many seconds since the Unix epoch (the extent of std's calendar support)
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();

// chrono: actual calendar operations — parsing, formatting, timezones, date math
// use chrono::{Utc, Duration as ChronoDuration};
// let now = Utc::now();
// let tomorrow = now + ChronoDuration::days(1);
// println!("{}", now.format("%Y-%m-%d %H:%M:%S"));

The dividing line is clean: std::time for measuring and timing; chrono (or time) for dates, calendars, time zones, and formatting. Don't try to do calendar math on SystemTime by juggling Durations, day/month/year arithmetic (leap years, DST, time zones) is genuinely hard and is exactly what chrono exists to get right. Conversely, don't pull in chrono just to time a function; Instant is simpler and correct for that.


Which Time Type to Use

You wantUse
Measure how long something tookInstant + .elapsed()
A timeout / deadline / rate limitInstant + Duration
A timestamp of when something happenedSystemTime
Seconds since the Unix epochSystemTime::duration_since(UNIX_EPOCH)
A span of time (sleep, timeout arg)Duration (from_secs/from_millis)
Subtraction that might underflowsaturating_sub / checked_sub
Dates, calendars, time zones, formattingthe chrono (or time) crate

The mental model: Instant for durations, SystemTime for timestamps, Duration for spans, chrono for calendars. Confusing the first two is the classic bug, elapsed time belongs to the monotonic clock, and only the monotonic clock is immune to the wall clock lurching around underneath you.


Key Takeaways

  • Rust has two clocks: Instant (monotonic, only moves forward, for elapsed time) and SystemTime (wall clock, can jump backward, for timestamps). Using the wrong one is a correctness bug, not a style choice.
  • Never measure elapsed time with SystemTime, a backward clock adjustment yields negative/garbage intervals (its duration_since returns a Result for exactly this reason). Use Instant::elapsed().
  • Duration is a nanosecond-precise, non-negative span, the currency of sleep, timeouts, and rate limits. Build with from_secs/from_millis, read with as_secs/as_secs_f64.
  • Plain +/- on durations and instants panic on overflow/underflow; use saturating_sub (clamp to zero) or checked_sub (returns Option) whenever a value is computed or from input.
  • std::time has no calendar support (no years, time zones, or formatting); use chrono/time for dates and human-readable time, and keep std::time for measuring and timing.