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:
Instantis 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?"SystemTimeis 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
SystemTimeto measure how long something took. Because the wall clock can move backward,SystemTime::now()minus an earlierSystemTimecan yield a negative interval, which is why the subtraction returns aResult(duration_sincegivesErrif time went backwards), not a bareDuration. For any "elapsed" measurement, useInstant, which is monotonic by construction and can't produce this bug. ReserveSystemTimefor 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 - nowpanics the momentnowpasses the deadline. Usesaturating_subwhen "clamp to zero" is the right behavior (a countdown that shouldn't go negative), orchecked_subwhen 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 want | Use |
|---|---|
| Measure how long something took | Instant + .elapsed() |
| A timeout / deadline / rate limit | Instant + Duration |
| A timestamp of when something happened | SystemTime |
| Seconds since the Unix epoch | SystemTime::duration_since(UNIX_EPOCH) |
| A span of time (sleep, timeout arg) | Duration (from_secs/from_millis) |
| Subtraction that might underflow | saturating_sub / checked_sub |
| Dates, calendars, time zones, formatting | the 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) andSystemTime(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 (itsduration_sincereturns aResultfor exactly this reason). UseInstant::elapsed(). Durationis a nanosecond-precise, non-negative span, the currency ofsleep, timeouts, and rate limits. Build withfrom_secs/from_millis, read withas_secs/as_secs_f64.- Plain
+/-on durations and instants panic on overflow/underflow; usesaturating_sub(clamp to zero) orchecked_sub(returnsOption) whenever a value is computed or from input. std::timehas no calendar support (no years, time zones, or formatting); usechrono/timefor dates and human-readable time, and keepstd::timefor measuring and timing.