Logo
Logo

Atharva Pandey/Lesson 8: std::time — Duration, Instant, SystemTime

Created Tue, 01 Oct 2024 07:50:00 +0000 Modified Tue, 01 Oct 2024 07:50:00 +0000

A few months ago I tracked down a bug where a cache TTL check was wrong because someone compared SystemTime values that had been serialized and deserialized across a system clock adjustment. The cached timestamps jumped backward, and suddenly entries that should’ve expired were “fresh” again. That’s the kind of lesson that teaches you the difference between monotonic clocks and wall clocks.

Two Clocks, Two Types

Rust gives you two time types because computers have two fundamentally different clocks:

  • Instant — monotonic clock. Always moves forward. Perfect for measuring elapsed time. Cannot be converted to a date or time of day. Not affected by NTP adjustments or daylight saving time.

  • SystemTime — wall clock. Represents a point in civil time. Can go backward (NTP corrections, manual clock changes). Use it when you need timestamps that are meaningful to humans.

This distinction exists in every operating system, but most languages paper over it. Rust makes you choose, which means you can’t accidentally use the wrong one.

Duration — The Measurement Unit

Duration represents a span of time. Both Instant and SystemTime use Duration for their arithmetic.

use std::time::Duration;

fn main() {
    // Creating durations
    let five_secs = Duration::from_secs(5);
    let half_sec = Duration::from_millis(500);
    let tiny = Duration::from_micros(100);
    let tinier = Duration::from_nanos(500);

    // Fractional seconds
    let precise = Duration::from_secs_f64(2.5);
    println!("2.5 seconds = {}ms", precise.as_millis());

    // Accessing components
    println!("5 secs: {} seconds, {} nanos", five_secs.as_secs(), five_secs.subsec_nanos());
    println!("As millis: {}", five_secs.as_millis());
    println!("As micros: {}", five_secs.as_micros());
    println!("As f64: {}", five_secs.as_secs_f64());

    // Arithmetic
    let total = five_secs + half_sec;
    println!("5s + 500ms = {}ms", total.as_millis());

    let doubled = five_secs * 2;
    println!("5s * 2 = {}s", doubled.as_secs());

    let halved = five_secs / 2;
    println!("5s / 2 = {}ms", halved.as_millis());

    // checked_add for overflow-safe arithmetic
    let big = Duration::from_secs(u64::MAX - 10);
    match big.checked_add(Duration::from_secs(20)) {
        Some(result) => println!("Result: {result:?}"),
        None => println!("Would overflow!"),
    }

    // Zero duration
    let zero = Duration::ZERO;
    println!("Is zero: {}", zero.is_zero());

    // Maximum duration
    let max = Duration::MAX;
    println!("Max duration: {} seconds", max.as_secs());
}

Instant — Measuring Elapsed Time

Instant is what you want 90% of the time. Benchmarking, timeouts, rate limiting, cooldowns — anything where you need to know how much time has passed.

use std::time::Instant;
use std::thread;

fn main() {
    // Basic timing
    let start = Instant::now();

    // Do some work
    let mut sum: u64 = 0;
    for i in 0..10_000_000 {
        sum += i;
    }

    let elapsed = start.elapsed();
    println!("Computation took: {elapsed:?}");
    println!("Result: {sum}");

    // elapsed() is sugar for Instant::now() - start
    let start = Instant::now();
    thread::sleep(std::time::Duration::from_millis(100));
    let end = Instant::now();
    println!("Sleep took: {:?}", end - start);
    println!("Same thing: {:?}", start.elapsed());
}

Practical: Function Timer

use std::time::Instant;

fn timed<F, R>(name: &str, f: F) -> R
where
    F: FnOnce() -> R,
{
    let start = Instant::now();
    let result = f();
    let elapsed = start.elapsed();
    println!("[{name}] {elapsed:?}");
    result
}

fn fibonacci(n: u64) -> u64 {
    if n <= 1 { return n; }
    fibonacci(n - 1) + fibonacci(n - 2)
}

fn main() {
    let result = timed("fibonacci(30)", || fibonacci(30));
    println!("fib(30) = {result}");

    let result = timed("fibonacci(35)", || fibonacci(35));
    println!("fib(35) = {result}");

    // Sorting comparison
    let mut data: Vec<i32> = (0..100_000).rev().collect();
    timed("sort 100k items", || data.sort());

    let mut data: Vec<i32> = (0..100_000).rev().collect();
    timed("sort_unstable 100k items", || data.sort_unstable());
}

Rate Limiter with Instant

use std::time::{Duration, Instant};

struct RateLimiter {
    max_per_second: u32,
    window_start: Instant,
    count: u32,
}

impl RateLimiter {
    fn new(max_per_second: u32) -> Self {
        RateLimiter {
            max_per_second,
            window_start: Instant::now(),
            count: 0,
        }
    }

    fn allow(&mut self) -> bool {
        let now = Instant::now();

        // Reset window if a second has passed
        if now.duration_since(self.window_start) >= Duration::from_secs(1) {
            self.window_start = now;
            self.count = 0;
        }

        if self.count < self.max_per_second {
            self.count += 1;
            true
        } else {
            false
        }
    }
}

fn main() {
    let mut limiter = RateLimiter::new(5);

    for i in 0..10 {
        if limiter.allow() {
            println!("Request {i}: allowed");
        } else {
            println!("Request {i}: RATE LIMITED");
        }
    }
}

Deadline Pattern

use std::time::{Duration, Instant};

fn process_with_deadline(items: &[String], timeout: Duration) -> usize {
    let deadline = Instant::now() + timeout;
    let mut processed = 0;

    for item in items {
        if Instant::now() >= deadline {
            println!("Deadline reached after processing {processed} items");
            break;
        }

        // Simulate work
        std::thread::sleep(Duration::from_millis(10));
        processed += 1;
    }

    processed
}

fn main() {
    let items: Vec<String> = (0..1000).map(|i| format!("item-{i}")).collect();

    let count = process_with_deadline(&items, Duration::from_millis(50));
    println!("Processed {count} out of {} items", items.len());
}

SystemTime — Wall Clock Timestamps

SystemTime represents a point on the civil clock. Use it when you need to record when something happened in terms humans understand.

use std::time::{SystemTime, UNIX_EPOCH, Duration};

fn main() {
    let now = SystemTime::now();
    println!("System time: {now:?}");

    // Duration since Unix epoch
    match now.duration_since(UNIX_EPOCH) {
        Ok(duration) => {
            println!("Unix timestamp: {}", duration.as_secs());
            println!("Unix timestamp (ms): {}", duration.as_millis());
        }
        Err(e) => {
            // This happens if the clock is set before 1970 somehow
            println!("Clock error: {e}");
        }
    }

    // Create a SystemTime from a Unix timestamp
    let timestamp = 1700000000u64; // Nov 14, 2023
    let time = UNIX_EPOCH + Duration::from_secs(timestamp);
    println!("\nTimestamp {timestamp} = {time:?}");

    // Time comparison
    let earlier = UNIX_EPOCH + Duration::from_secs(1_000_000_000);
    let later = UNIX_EPOCH + Duration::from_secs(1_700_000_000);

    match later.duration_since(earlier) {
        Ok(diff) => println!("Difference: {} days", diff.as_secs() / 86400),
        Err(_) => println!("Clock went backward!"),
    }
}

Why duration_since Returns Result

Here’s the important part — SystemTime::duration_since() returns Result, not just Duration. That’s because the wall clock can go backward:

use std::time::{SystemTime, Duration, UNIX_EPOCH};

fn main() {
    let time_a = UNIX_EPOCH + Duration::from_secs(1000);
    let time_b = UNIX_EPOCH + Duration::from_secs(500);

    // This fails because time_a is after time_b
    match time_b.duration_since(time_a) {
        Ok(d) => println!("Duration: {d:?}"),
        Err(e) => {
            println!("System time went backward by {:?}", e.duration());
        }
    }

    // elapsed() can also "fail" — returns Ok(0) in that case
    // In practice, the clock rarely jumps backward, but NTP corrections
    // and VM snapshots can cause it
    let now = SystemTime::now();
    match now.elapsed() {
        Ok(d) => println!("Elapsed since 'now': {d:?}"),
        Err(e) => println!("Clock jumped backward: {e}"),
    }
}

This is why Instant exists — it uses a monotonic clock that guarantees time never goes backward. For measuring elapsed time, always use Instant. For timestamps in logs, databases, or APIs, use SystemTime.

File Timestamps

SystemTime shows up in file metadata:

use std::fs;
use std::io;
use std::time::{SystemTime, UNIX_EPOCH};

fn format_system_time(t: SystemTime) -> String {
    match t.duration_since(UNIX_EPOCH) {
        Ok(d) => {
            let secs = d.as_secs();
            // Very rough formatting — use chrono for real code
            let days_since_epoch = secs / 86400;
            let time_of_day = secs % 86400;
            let hours = time_of_day / 3600;
            let minutes = (time_of_day % 3600) / 60;
            format!("Day {days_since_epoch} {hours:02}:{minutes:02} UTC")
        }
        Err(_) => "before epoch".to_string(),
    }
}

fn main() -> io::Result<()> {
    let metadata = fs::metadata("Cargo.toml")?;

    if let Ok(modified) = metadata.modified() {
        println!("Modified: {}", format_system_time(modified));
    }

    if let Ok(created) = metadata.created() {
        println!("Created:  {}", format_system_time(created));
    }

    if let Ok(accessed) = metadata.accessed() {
        println!("Accessed: {}", format_system_time(accessed));
    }

    Ok(())
}

For real date/time formatting, you want the chrono or time crate. The standard library deliberately doesn’t include calendar types — dates are surprisingly complex (time zones, leap seconds, daylight saving), and Rust’s philosophy is to keep the stdlib small.

Benchmarking Pattern

A simple but effective micro-benchmark:

use std::time::{Duration, Instant};

fn bench<F: FnMut()>(name: &str, iterations: u32, mut f: F) {
    // Warmup
    for _ in 0..iterations / 10 {
        f();
    }

    let start = Instant::now();
    for _ in 0..iterations {
        f();
    }
    let total = start.elapsed();
    let per_op = total / iterations;

    println!(
        "{name}: {total:?} total, {per_op:?} per op ({iterations} iterations)"
    );
}

fn main() {
    let data: Vec<i32> = (0..10_000).collect();

    bench("Vec::contains", 1000, || {
        let _ = data.contains(&9999);
    });

    bench("binary_search", 1000, || {
        let _ = data.binary_search(&9999);
    });

    bench("iterator find", 1000, || {
        let _ = data.iter().find(|&&x| x == 9999);
    });

    // String allocation
    bench("String::from", 10_000, || {
        let _ = String::from("hello world this is a test string");
    });

    bench("&str (no alloc)", 10_000, || {
        let _: &str = "hello world this is a test string";
    });
}

For serious benchmarking, use criterion — it handles warm-up, statistical analysis, and noise reduction. But this pattern is great for quick sanity checks during development.

Timeout Wrapper

use std::time::{Duration, Instant};
use std::thread;
use std::sync::mpsc;

fn with_timeout<F, T>(timeout: Duration, f: F) -> Option<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,
{
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let result = f();
        let _ = tx.send(result);
    });

    match rx.recv_timeout(timeout) {
        Ok(result) => Some(result),
        Err(_) => None,
    }
}

fn main() {
    // This completes in time
    let result = with_timeout(Duration::from_secs(2), || {
        thread::sleep(Duration::from_millis(100));
        42
    });
    println!("Fast task: {result:?}"); // Some(42)

    // This times out
    let result = with_timeout(Duration::from_millis(50), || {
        thread::sleep(Duration::from_secs(5));
        42
    });
    println!("Slow task: {result:?}"); // None
}

The Mental Model

Here’s how I think about it:

  • Need to measure how long something takes? Use Instant.
  • Need to implement a timeout or deadline? Use Instant.
  • Need to record when something happened? Use SystemTime.
  • Need to compare timestamps across processes or machines? Use SystemTime (with awareness that clocks can drift).
  • Need to do math with time spans? Use Duration.

The standard library gives you the primitives. For anything involving calendars, time zones, or human-readable formatting, reach for a crate. That split is intentional — the hard part of time isn’t measuring it, it’s representing it for humans.