Measuring elapsed time with Instant and Duration
You are debugging a web handler that feels sluggish. You suspect the database query is the bottleneck, but "feels" isn't enough. You need numbers. You grab a timestamp before the query and another after. Now you have two points in time. You need the gap between them.
In Rust, you don't subtract timestamps like integers. You use std::time::Instant to capture moments and std::time::Duration to represent the span between them. This separation keeps your code safe from clock adjustments and makes the intent clear.
Instant is your monotonic stopwatch
Instant represents a specific moment in time measured by the system's monotonic clock. The word "monotonic" is the key. A monotonic clock only moves forward. It never jumps backward, even if the user changes the system time or NTP syncs the clock.
Think of Instant like the odometer in a car. The odometer counts up as you drive. It doesn't care what time it is on the dashboard clock. If someone resets the dashboard clock to 12:00, the odometer keeps counting miles. Instant behaves the same way. It measures elapsed time, not wall-clock time.
This property makes Instant the correct tool for benchmarks, timeouts, and measuring how long a function takes. You don't want your benchmark to break because the system clock jumped backward by five minutes during a sync.
SystemTime is different. SystemTime represents the wall clock. It can jump forward or backward. Use SystemTime when you need to record a timestamp for a user, save an event to a database, or convert to a human-readable format. Never use SystemTime to measure elapsed time.
Trust the monotonic clock. It never lies about how much time passed.
Duration holds the span
Duration represents a time interval. It is not a point in time. It is a quantity. A Duration can be added to another Duration, multiplied by a scalar, or subtracted from an Instant (in the form of Instant - Instant).
Internally, Duration stores two fields: secs as a u64 and nanos as a u32. This structure gives you nanosecond precision while supporting extremely long intervals. The maximum duration is roughly 584 years. You will rarely hit this limit.
The nanos field is always less than one billion. Rust normalizes durations so the nanoseconds never overflow into the seconds. This invariant simplifies arithmetic. You don't need to carry over nanoseconds manually.
Convention aside: When logging durations, use the debug format {:?}. It prints output like 2s 500ms, which is readable and precise. Converting to seconds manually loses precision and adds noise to your logs.
Treat Duration as a quantity, not a moment. You add quantities; you subtract moments.
Minimal example
The most common operation is subtracting two Instant values to get a Duration. The subtraction operator is overloaded for this purpose.
use std::time::{Duration, Instant};
fn main() {
// Capture the moment before work starts.
let start = Instant::now();
// Simulate some work.
std::thread::sleep(Duration::from_secs(2));
// Capture the moment work finishes.
let end = Instant::now();
// Subtracting two Instants yields a Duration.
// The compiler knows end - start produces a Duration.
let elapsed = end - start;
println!("Elapsed: {:?}", elapsed);
}
The output looks like Elapsed: 2s 1ns. The extra nanosecond comes from the overhead of the sleep call and the time between sleep returning and Instant::now() executing. This is expected. Hardware timers have finite resolution.
Walkthrough: subtraction and panics
When you write end - start, Rust performs a runtime check. The subtraction calculates the difference between the two instants. If end is before start, the subtraction panics.
This panic is a safety feature. Elapsed time cannot be negative. If you see a panic here, your logic is wrong. You likely swapped the order of subtraction or measured across a scope boundary incorrectly.
The compiler cannot catch this error at compile time. start and end are both Instant values. The compiler sees two values of the same type and allows the subtraction. The order only matters at runtime.
If you are unsure about the order, use checked_duration_since. This method returns an Option<Duration> instead of panicking.
use std::time::Instant;
fn main() {
let start = Instant::now();
std::thread::sleep(std::time::Duration::from_millis(100));
let end = Instant::now();
// Safe subtraction. Returns None if start is after end.
if let Some(elapsed) = end.checked_duration_since(start) {
println!("Safe elapsed: {:?}", elapsed);
} else {
println!("Time went backward? Check your logic.");
}
}
Instant also provides elapsed(), which is a shorthand for Instant::now() - start. This is useful when you want to measure time from a fixed point without storing the end instant.
use std::time::Instant;
fn main() {
let start = Instant::now();
std::thread::sleep(std::time::Duration::from_millis(500));
// Equivalent to Instant::now() - start.
let elapsed = start.elapsed();
println!("Elapsed: {:?}", elapsed);
}
If you can't guarantee the order, use checked_duration_since. A panic is a bug; a None is a handled edge case.
Realistic benchmarking helper
In real code, you often want to measure multiple functions or wrap logic in a reusable helper. A closure-based benchmark function is a common pattern.
use std::time::Instant;
/// Measures how long a closure takes to execute.
/// Returns the result and the elapsed duration.
fn measure<T>(f: impl FnOnce() -> T) -> (T, std::time::Duration) {
let start = Instant::now();
let result = f();
let elapsed = Instant::now() - start;
(result, elapsed)
}
fn main() {
// Measure a computation.
let (sum, duration) = measure(|| {
let mut total = 0;
for i in 0..100_000 {
total += i;
}
total
});
println!("Sum is {}, took {:?}", sum, duration);
// Measure a fallible operation.
let (result, duration) = measure(|| {
std::fs::read_to_string("Cargo.toml")
});
match result {
Ok(content) => println!("Read {} bytes in {:?}", content.len(), duration),
Err(e) => println!("Error: {}", e),
}
}
This pattern isolates the timing logic. You can reuse measure anywhere. The closure captures the work, and the function returns both the result and the cost.
Convention aside: Keep Instant::now() calls as close to the work as possible. Any code between the start capture and the work adds noise. Any code between the work and the end capture adds noise. The helper function above minimizes this by capturing immediately before and after the closure call.
Duration arithmetic and conversion
You can perform arithmetic on Duration values. You can add two durations, multiply a duration by a scalar, or check if a duration exceeds a limit.
use std::time::Duration;
fn main() {
let d1 = Duration::from_secs(1);
let d2 = Duration::from_millis(500);
// Adding durations.
let total = d1 + d2;
println!("Total: {:?}", total); // 1s 500ms
// Multiplying by a scalar.
let doubled = d1 * 2;
println!("Doubled: {:?}", doubled); // 2s
// Converting to seconds as a float.
let secs_f64 = total.as_secs_f64();
println!("Seconds: {}", secs_f64); // 1.5
}
Duration does not implement division by another Duration. If you need a ratio, convert both to f64 seconds and divide. This is a deliberate design choice. Division of durations can lose precision and introduce floating-point errors. The standard library forces you to acknowledge this trade-off explicitly.
Convention aside: Use as_secs_f64() when you need a scalar for ratios or plotting. Use as_millis() or as_micros() when you need integer precision for logging or protocol buffers. Avoid as_nanos() unless you need nanosecond precision, as it returns a u128 and can be slower on 32-bit systems.
Duration arithmetic can overflow. If you add two durations and the result exceeds the maximum, the operation panics in debug mode and wraps in release mode. Use checked_add if you need to handle overflow gracefully.
use std::time::Duration;
fn main() {
let huge = Duration::from_secs(u64::MAX);
let one = Duration::from_secs(1);
// Safe addition. Returns None on overflow.
let result = huge.checked_add(one);
assert!(result.is_none());
}
SystemTime is for wall clocks, not timers
Developers often reach for SystemTime when they need to measure time. This is a mistake. SystemTime represents the wall clock, which can jump.
If you subtract two SystemTime values, the result is a Result<Duration, SystemTimeError>. The subtraction can fail if the second time is before the first. This happens when the system clock is adjusted backward.
use std::time::SystemTime;
fn main() {
let start = SystemTime::now();
std::thread::sleep(std::time::Duration::from_secs(1));
let end = SystemTime::now();
// Returns Result because the clock might have jumped.
match end.duration_since(start) {
Ok(elapsed) => println!("Elapsed: {:?}", elapsed),
Err(e) => println!("Clock jumped backward: {}", e),
}
}
The duration_since method returns an error if start is after end. This is different from Instant, which panics. SystemTime forces you to handle the error because clock adjustments are a normal part of system operation.
Never use SystemTime for benchmarks. The clock will jump, and your measurements will break. Use Instant for anything that measures elapsed time. Use SystemTime only when you need to record a timestamp for external consumption.
Pitfalls and safe alternatives
The most common pitfall is subtracting in the wrong order. start - end panics. The compiler cannot help here. You must ensure the order is correct.
Another pitfall is assuming Duration has infinite precision. Duration stores nanoseconds, but the underlying hardware timer might have lower resolution. On some systems, Instant::now() might return the same value for rapid calls. This is normal. Don't rely on nanosecond precision for micro-benchmarks.
If you need to compare durations, use the comparison operators. Duration implements PartialOrd and Ord.
use std::time::Duration;
fn main() {
let d1 = Duration::from_secs(1);
let d2 = Duration::from_millis(500);
if d1 > d2 {
println!("d1 is longer");
}
}
Be careful with Duration::from_secs_f64. This constructor truncates the fractional part. If you pass 1.9, you get 1 second, not 2. Use Duration::from_secs and Duration::from_nanos separately if you need to preserve precision.
use std::time::Duration;
fn main() {
// Truncates to 1 second.
let d = Duration::from_secs_f64(1.9);
println!("{:?}", d); // 1s
// Preserve precision.
let d_precise = Duration::new(1, 900_000_000);
println!("{:?}", d_precise); // 1s 900ms
}
If you try to subtract a Duration from an Instant, the compiler rejects it with E0308 (mismatched types). You can only subtract an Instant from another Instant. To move an Instant forward, you must use checked_add on the Duration and add it to the Instant via Instant + Duration, which returns a new Instant.
use std::time::{Duration, Instant};
fn main() {
let start = Instant::now();
let delay = Duration::from_secs(1);
// Adding Duration to Instant returns a new Instant.
let future = start + delay;
println!("Future: {:?}", future);
}
Decision matrix
Use Instant when you need to measure elapsed time, benchmark code, or track timeouts. The monotonic clock guarantees the time only moves forward.
Use SystemTime when you need to record a timestamp for a user, save an event to a database, or convert to wall-clock formats like ISO 8601.
Use Duration when you need to represent a span of time, such as a delay, a timeout, or the result of subtracting two instants.
Use checked_duration_since when you cannot guarantee the order of instants. A panic is a bug; a None is a handled edge case.
Use chrono or time crates when you need calendar arithmetic, time zones, or parsing human-readable date strings. The standard library handles instants and durations, not calendars.
Pick the tool that matches the job. Elapsed time needs Instant. Wall clock needs SystemTime. Mixing them causes bugs.