Time & Duration #

Almost every moderately complex program needs to deal with time — measuring how long an operation takes, determining whether a deadline has been missed, applying timeouts to network requests, or keeping track of when an event occurs. Rust provides two intentionally separate time concepts in the standard library: Instant for monotonic duration measurements (always progressing, not affected by changes in the system clock), and SystemTime for calendar time that can be compared with real world time. These differences are not coincidental — mixing them is a common source of bugs in other languages. This article discusses both along with Duration as a representation of time intervals, idiomatic patterns of their use, and how to handle calendar time more fully with the chrono crate.

Duration — Represents a Time Interval #

Duration is a type that represents a non-negative time interval. It stores data as a number of seconds and nanoseconds, providing precision down to one nanosecond.

use std::time::Duration;

fn main() {
    // Creates Duration from various units
    let satu_detik = Duration::from_secs(1);
    let setengah_detik = Duration::from_millis(500);
    let satu_menit = Duration::from_secs(60);
    let satu_jam = Duration::from_secs(3600);
    let presisi_tinggi = Duration::from_nanos(1_500_000);  // 1.5 milliseconds
    let dari_float = Duration::from_secs_f64(1.5);         // 1.5 sec

    // Useful constants
    let nol = Duration::ZERO;
    let maks = Duration::MAX;

    println!("Satu detik: {:?}", satu_detik);         // 1s
    println!("Setengah detik: {:?}", setengah_detik); // 500ms

    // Access components
    let durasi = Duration::from_millis(1_500);  // 1.5 sec
    println!("Detik: {}", durasi.as_secs());           // 1
    println!("Subsecond nanos: {}", durasi.subsec_nanos()); // 500_000_000
    println!("Total milidetik: {}", durasi.as_millis()); // 1500
    println!("Total nanodetik: {}", durasi.as_nanos());  // 1_500_000_000
    println!("Sebagai f64 detik: {}", durasi.as_secs_f64()); // 1.5

    // Arithmetic Duration
    let d1 = Duration::from_secs(10);
    let d2 = Duration::from_secs(3);

    let penjumlahan = d1 + d2;       // 13 seconds
    let pengurangan = d1 - d2;       // 7 seconds
    let perkalian = d1 * 3;          // 30 seconds
    let pembagian = d1 / 2;          // 5 seconds

    println!("{:?}", penjumlahan);  // 13s
    println!("{:?}", pembagian);    // 5s

    // Checked arithmetic — returns None if overflow or underflow
    let hasil = d2.checked_sub(d1);  // None — invalid negative result
    println!("{:?}", hasil);          // None

    let hasil = d1.checked_sub(d2);  // Some(7s)
    println!("{:?}", hasil);          // Some(7s)

    // Saturating arithmetic — results are limited between ZERO and MAX
    let hasil = d2.saturating_sub(d1);  // Duration::ZERO, not underflow
    println!("{:?}", hasil);             // 0ns

    // Comparison
    println!("{}", d1 > d2);   // true
    println!("{}", d1 == Duration::from_millis(10_000));  // true
}

Instant — Monotonic Time Measurement #

Instant represents a point in time that is guaranteed to be monotonic — its value always increases, never decreases. This should be used to measure operation duration, not SystemTime which can fall back when the system clock is reset.

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

fn main() {
    // Take the current point in time
    let mulai = Instant::now();

    // Perform the operation you want to measure
    operasi_yang_ingin_diukur();

    // Calculate elapsed time
    let durasi = mulai.elapsed();
    println!("Operasi memakan waktu: {:?}", durasi);
    println!("Dalam milidetik: {}", durasi.as_millis());

    // elapsed() is equivalent to Instant::now() - start
    let sekarang = Instant::now();
    let manual = sekarang - mulai;

    // Instant can be used for deadlines
    let batas_waktu = Instant::now() + Duration::from_secs(5);
    while Instant::now() < batas_waktu {
        // do something until time runs out
        thread::sleep(Duration::from_millis(100));
        if kondisi_selesai() {
            break;
        }
    }

    // Measures multiple operations
    let operasi = vec!["kecil", "sedang", "besar"];
    for nama in &operasi {
        let t = Instant::now();
        simulasi_operasi(nama);
        println!("{}: {:?}", nama, t.elapsed());
    }
}

fn operasi_yang_ingin_diukur() {
    thread::sleep(Duration::from_millis(50));
}

fn kondisi_selesai() -> bool { false }

fn simulasi_operasi(nama: &str) {
    let durasi = match nama {
        &"kecil" => Duration::from_millis(10),
        &"sedang" => Duration::from_millis(50),
        _ => Duration::from_millis(100),
    };
    thread::sleep(durasi);
}

Why Instant, Not SystemTime, for Measurement #

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

fn main() {
    // ANTI-PATTERN: uses SystemTime to measure duration
    let mulai = SystemTime::now();
    operasi_lambat();
    let durasi = SystemTime::now().duration_since(mulai);
    // duration_since returns Result because SystemTime can go backwards!
    // If the system clock is set to the past during operation, the result is Err

    // CORRECT: Instant is always monotonic, never backwards
    let mulai = Instant::now();
    operasi_lambat();
    let durasi = mulai.elapsed();  // always works, no need to unwrap
    println!("{:?}", durasi);
}

fn operasi_lambat() {
    std::thread::sleep(std::time::Duration::from_millis(10));
}
flowchart TD
    A{Kebutuhan waktu?} --> B{Perlu waktu kalender?}
    B -- Yes --> C["SystemTime\nWaktu sejak UNIX epoch\nBisa dibandingkan dengan tanggal"]
    B -- No --> D{Mengukur durasi operasi?}
    D -- Yes --> E["Instant::now()\nMonotonic, tidak bisa mundur\nGunakan .elapsed()"]
    D -- No --> F{Interval tetap?}
    F -- Yes --> G["Duration::from_secs(n)\nInterval waktu yang fix"]
    F -- No --> H["Kombinasi Instant + Duration\nDeadline dan timeout"]

    style C fill:#fff3e0
    style E fill:#e8f5e9
    style G fill:#e3f2fd
    style H fill:#e8f5e9

SystemTime — Calendar Time #

SystemTime represents a point in time based on the system clock. Use it when you need time that can be communicated to the outside world — timestamp files, expiry tokens, logging with time.

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

fn main() {
    // Current time
    let sekarang = SystemTime::now();

    // Convert to UNIX timestamp (seconds since January 1, 1970 UTC)
    let unix_timestamp = sekarang
        .duration_since(UNIX_EPOCH)
        .expect("Waktu sebelum UNIX epoch");

    println!("UNIX timestamp: {}", unix_timestamp.as_secs());
    println!("Dengan milidetik: {}", unix_timestamp.as_millis());

    // Creates a SystemTime from a UNIX timestamp
    let timestamp: u64 = 1_700_000_000;  // timestamp example
    let waktu = UNIX_EPOCH + Duration::from_secs(timestamp);
    println!("{:?}", waktu);

    // SystemTime Arithmetic
    let satu_jam_lalu = SystemTime::now() - Duration::from_secs(3600);
    let satu_jam_lagi = SystemTime::now() + Duration::from_secs(3600);

    // duration_since returns Result — may fail if time sequence is incorrect
    match satu_jam_lagi.duration_since(SystemTime::now()) {
        Ok(durasi) => println!("Dalam {} detik lagi", durasi.as_secs()),
        Err(e) => println!("Waktu sudah lewat {} detik yang lalu", e.duration().as_secs()),
    }

    // Checks whether a certain time has passed
    fn sudah_kadaluarsa(expiry: SystemTime) -> bool {
        SystemTime::now() > expiry
    }

    let token_expiry = SystemTime::now() + Duration::from_secs(3600);
    println!("Token kadaluarsa: {}", sudah_kadaluarsa(token_expiry));  // false

    // File metadata — modification time
    use std::fs;
    if let Ok(metadata) = fs::metadata("Cargo.toml") {
        if let Ok(waktu_modif) = metadata.modified() {
            let umur = SystemTime::now()
                .duration_since(waktu_modif)
                .unwrap_or(Duration::ZERO);
            println!("File terakhir dimodifikasi {} detik lalu", umur.as_secs());
        }
    }
}

Timeout on Concurrent Operations #

Timeout is one of the most frequent uses of Duration and Instant in real code — especially when working with channels, locks, or networks.

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

fn main() {
    // Timeout on recv channel
    let (tx, rx) = mpsc::channel::<String>();

    thread::spawn(move || {
        thread::sleep(Duration::from_millis(200));
        tx.send(String::from("pesan terlambat")).unwrap();
    });

    match rx.recv_timeout(Duration::from_millis(100)) {
        Ok(pesan) => println!("Diterima: {}", pesan),
        Err(mpsc::RecvTimeoutError::Timeout) => println!("Timeout — pesan tidak datang tepat waktu"),
        Err(mpsc::RecvTimeoutError::Disconnected) => println!("Sender sudah ter-drop"),
    }

    // Timeout on Mutex lock
    let mutex = Arc::new(Mutex::new(0i32));
    let mutex_clone = Arc::clone(&mutex);

    // Simulation of threads holding a long lock
    let _guard_holder = thread::spawn(move || {
        let _lock = mutex_clone.lock().unwrap();
        thread::sleep(Duration::from_millis(500));
        // lock is released when the thread is finished
    });

    thread::sleep(Duration::from_millis(10));  // give another thread time to take the lock

    // try_lock with retry loop and deadline
    let deadline = Instant::now() + Duration::from_millis(100);
    let berhasil = loop {
        match mutex.try_lock() {
            Ok(mut nilai) => {
                *nilai += 1;
                break true;
            }
            Err(_) => {
                if Instant::now() >= deadline {
                    break false;
                }
                thread::sleep(Duration::from_millis(10));
            }
        }
    };
    println!("Lock berhasil diperoleh: {}", berhasil);

    // Retry with exponential backoff
    fn operasi_dengan_retry<F, T, E>(
        operasi: F,
        maks_percobaan: u32,
        backoff_awal: Duration,
    ) -> Result<T, E>
    where
        F: Fn() -> Result<T, E>,
    {
        let mut backoff = backoff_awal;
        for percobaan in 0..maks_percobaan {
            match operasi() {
                Ok(hasil) => return Ok(hasil),
                Err(e) if percobaan + 1 == maks_percobaan => return Err(e),
                Err(_) => {
                    println!("Percobaan {} gagal, menunggu {:?}", percobaan + 1, backoff);
                    thread::sleep(backoff);
                    backoff = backoff.saturating_mul(2);  // doubles every retry
                }
            }
        }
        unreachable!()
    }

    let mut percobaan = 0;
    let hasil = operasi_dengan_retry(
        || -> Result<i32, &str> {
            percobaan += 1;
            if percobaan < 3 { Err("gagal") } else { Ok(42) }
        },
        5,
        Duration::from_millis(10),
    );
    println!("Hasil setelah retry: {:?}", hasil);
}

Simple Benchmarking #

Instant is the foundation for measuring code performance. Although for serious benchmarking you need a crate like criterion, a quick measurement with Instant is useful for initial validation.

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

// Simple helper for benchmarking
fn ukur_waktu<F: Fn()>(nama: &str, iterasi: u32, fungsi: F) {
    // Warm-up — let the CPU cache and branch predictor adapt
    for _ in 0..10 {
        fungsi();
    }

    let mulai = Instant::now();
    for _ in 0..iterasi {
        fungsi();
    }
    let total = mulai.elapsed();
    let per_iterasi = total / iterasi;

    println!("{}: total {:?}, per iterasi {:?}", nama, total, per_iterasi);
}

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

    // Compare the two approaches
    let data_clone = data.clone();
    ukur_waktu("iter sum", 1000, || {
        let _: i32 = data_clone.iter().sum();
    });

    let data_clone = data.clone();
    ukur_waktu("fold manual", 1000, || {
        let _: i32 = data_clone.iter().fold(0, |acc, &x| acc + x);
    });

    let data_clone = data.clone();
    ukur_waktu("loop manual", 1000, || {
        let mut total = 0i32;
        for &x in &data_clone {
            total += x;
        }
        let _ = total;
    });

    // Measures non-loopable operations trivially
    struct Timer {
        nama: String,
        mulai: Instant,
    }

    impl Timer {
        fn baru(nama: &str) -> Self {
            Timer {
                nama: nama.to_string(),
                mulai: Instant::now(),
            }
        }
    }

    impl Drop for Timer {
        fn drop(&mut self) {
            println!("[{}] selesai dalam {:?}", self.nama, self.mulai.elapsed());
        }
    }

    // Timer is automatically printed when exiting the scope
    {
        let _t = Timer::baru("inisialisasi database");
        // work simulation
        std::thread::sleep(Duration::from_millis(50));
    }  // "database initialization: completed in 50ms" is printed here

    {
        let _t = Timer::baru("load konfigurasi");
        std::thread::sleep(Duration::from_millis(20));
    }
}

Time Formatting and Parsing with Chrono #

The Rust standard library deliberately does not include date formatting and timezone parsing due to its complexity. For these needs, the chrono crate is the standard choice in the Rust ecosystem.

# Cargo.toml
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
use chrono::{DateTime, Local, Utc, NaiveDate, NaiveDateTime, TimeZone, Duration};

fn main() {
    // Current time
    let sekarang_utc: DateTime<Utc> = Utc::now();
    let sekarang_lokal: DateTime<Local> = Local::now();

    println!("UTC: {}", sekarang_utc);
    println!("Lokal: {}", sekarang_lokal);

    // Formatting — uses string formats such as strftime
    println!("{}", sekarang_utc.format("%Y-%m-%d %H:%M:%S"));  // 2024-01-15 10:30:00
    println!("{}", sekarang_lokal.format("%d/%m/%Y"));           // 01/15/2024
    println!("{}", sekarang_utc.format("%A, %B %d, %Y"));       // Monday, January 15, 2024

    // RFC 3339 (ISO 8601) format — for API and serialization
    println!("{}", sekarang_utc.to_rfc3339());  // 2024-01-15T10:30:00+00:00

    // Parsing of a string
    let dari_string: DateTime<Utc> = "2024-01-15T10:30:00Z"
        .parse::<DateTime<Utc>>()
        .expect("Format tidak valid");

    let dari_format = DateTime::parse_from_str(
        "15/01/2024 10:30:00 +0700",
        "%d/%m/%Y %H:%M:%S %z"
    ).expect("Format tidak valid");

    // NaiveDate — date without timezone
    let tanggal = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
    println!("{}", tanggal);  // 2024-01-15
    println!("Hari ke-{} dalam tahun ini", tanggal.ordinal());
    println!("Hari dalam seminggu: {}", tanggal.format("%A"));

    // Date arithmetic with chrono::Duration
    let besok = tanggal + Duration::days(1);
    let minggu_depan = tanggal + Duration::weeks(1);
    let bulan_depan = tanggal + Duration::days(30);  // chrono does not have Duration::months

    println!("Besok: {}", besok);
    println!("Minggu depan: {}", minggu_depan);

    // Date component
    use chrono::Datelike;
    println!("Tahun: {}", tanggal.year());
    println!("Bulan: {}", tanggal.month());
    println!("Hari: {}", tanggal.day());

    // Time component
    use chrono::Timelike;
    let waktu = sekarang_utc;
    println!("Jam: {}", waktu.hour());
    println!("Menit: {}", waktu.minute());
    println!("Detik: {}", waktu.second());
}

Convert Between std::time and Chrono #

use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Utc};

fn main() {
    // SystemTime → chrono DateTime
    let sys_time = SystemTime::now();
    let datetime: DateTime<Utc> = sys_time.into();
    println!("{}", datetime.to_rfc3339());

    // chrono DateTime → SystemTime
    let chrono_time: DateTime<Utc> = Utc::now();
    let sys_time: SystemTime = chrono_time.into();

    // UNIX timestamp → chrono
    let timestamp: i64 = 1_700_000_000;
    let datetime = DateTime::<Utc>::from_timestamp(timestamp, 0)
        .expect("Timestamp tidak valid");
    println!("{}", datetime.format("%Y-%m-%d %H:%M:%S UTC"));

    // chrono → UNIX timestamp
    let ts = Utc::now().timestamp();         // sec
    let ts_ms = Utc::now().timestamp_millis(); // milliseconds
    let ts_ns = Utc::now().timestamp_nanos_opt().unwrap(); // nanosecond
    println!("Timestamp: {}", ts);
}

Idiomatic Patterns with Time #

Some patterns that often arise when working with time in production Rust code.

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

fn main() {
    // 1. Simple rate limiting
    struct RateLimiter {
        interval: Duration,
        terakhir: Instant,
    }

    impl RateLimiter {
        fn baru(per_detik: u32) -> Self {
            RateLimiter {
                interval: Duration::from_secs(1) / per_detik,
                terakhir: Instant::now() - Duration::from_secs(1),
            }
        }

        fn izinkan(&mut self) -> bool {
            let sekarang = Instant::now();
            if sekarang.duration_since(self.terakhir) >= self.interval {
                self.terakhir = sekarang;
                true
            } else {
                false
            }
        }

        fn tunggu_dan_izinkan(&mut self) {
            let sekarang = Instant::now();
            let lewat = sekarang.duration_since(self.terakhir);
            if lewat < self.interval {
                thread::sleep(self.interval - lewat);
            }
            self.terakhir = Instant::now();
        }
    }

    let mut limiter = RateLimiter::baru(10);  // 10 requests per second
    for i in 0..5 {
        limiter.tunggu_dan_izinkan();
        println!("Request {} diizinkan", i);
    }

    // 2. Cache with TTL (Time To Live)
    struct EntriCache<T> {
        nilai: T,
        dibuat: Instant,
        ttl: Duration,
    }

    impl<T> EntriCache<T> {
        fn baru(nilai: T, ttl: Duration) -> Self {
            EntriCache {
                nilai,
                dibuat: Instant::now(),
                ttl,
            }
        }

        fn masih_valid(&self) -> bool {
            self.dibuat.elapsed() < self.ttl
        }

        fn nilai(&self) -> Option<&T> {
            if self.masih_valid() { Some(&self.nilai) } else { None }
        }
    }

    let cache = EntriCache::baru("data dari database", Duration::from_secs(300));
    match cache.nilai() {
        Some(data) => println!("Cache hit: {}", data),
        None => println!("Cache expired, perlu refresh"),
    }

    // 3. Measure the performance of a specific section of code
    struct Stopwatch {
        checkpoints: Vec<(String, Duration)>,
        mulai: Instant,
        terakhir: Instant,
    }

    impl Stopwatch {
        fn mulai() -> Self {
            let sekarang = Instant::now();
            Stopwatch {
                checkpoints: Vec::new(),
                mulai: sekarang,
                terakhir: sekarang,
            }
        }

        fn lap(&mut self, nama: &str) {
            let sekarang = Instant::now();
            self.checkpoints.push((nama.to_string(), sekarang - self.terakhir));
            self.terakhir = sekarang;
        }

        fn cetak_laporan(&self) {
            println!("=== Laporan Performa ===");
            for (nama, durasi) in &self.checkpoints {
                println!("  {}: {:?}", nama, durasi);
            }
            println!("  Total: {:?}", self.mulai.elapsed());
        }
    }

    let mut sw = Stopwatch::mulai();

    // Simulation of operation stages
    thread::sleep(Duration::from_millis(20));
    sw.lap("koneksi database");

    thread::sleep(Duration::from_millis(50));
    sw.lap("query data");

    thread::sleep(Duration::from_millis(10));
    sw.lap("proses hasil");

    sw.cetak_laporan();
    // === Performance Report ===
    // database connection: ~20ms
    // query data: ~50ms
    // processing results: ~10ms
    // Total: ~80ms

    // 4. Timestamp for logging
    fn log(level: &str, pesan: &str) {
        let ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis();
        println!("[{}] [{}] {}", ts, level, pesan);
    }

    log("INFO", "Aplikasi dimulai");
    log("WARN", "Koneksi lambat");
    log("ERROR", "Gagal terhubung ke database");
}

sleep and Time Precision #

thread::sleep is the simplest way to wait, but there are a few things to understand about its precision.

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

fn main() {
    // sleep is not guaranteed to be precise — the OS may delay longer
Actual ZZL6ZZ can be more than requested, depending on the OS scheduler
    let diminta = Duration::from_millis(10);
    let mulai = Instant::now();
    thread::sleep(diminta);
    let aktual = mulai.elapsed();
    println!("Diminta: {:?}, Aktual: {:?}", diminta, aktual);
    // can be more than requested, depending on the OS scheduler

    // Spin-wait for higher precision (but consumes CPU)
    fn sleep_presisi(durasi: Duration) {
        let deadline = Instant::now() + durasi;
        // Sleeps most of the time with normal sleep
        let batas_spin = duration_from_millis_sat(1);
        if durasi > batas_spin {
            thread::sleep(durasi - batas_spin);
        }
        // Spin for the remaining time
        while Instant::now() < deadline {
            std::hint::spin_loop();  // hints to the CPU that this is a busy-wait loop
        }
    }

    let mulai = Instant::now();
    sleep_presisi(Duration::from_millis(10));
    println!("Spin-sleep aktual: {:?}", mulai.elapsed());

    // yield_now — relinquish time slice to another thread without sleeping
    for _ in 0..1000 {
        thread::yield_now();  // give another thread a chance to run
    }

    // Non-drift loop interval
    fn jalankan_pada_interval(interval: Duration, iterasi: u32) {
        let mut deadline = Instant::now();
        for i in 0..iterasi {
            deadline += interval;
            println!("Iterasi {} pada {:?}", i, Instant::now().elapsed());
            let sekarang = Instant::now();
            if deadline > sekarang {
                thread::sleep(deadline - sekarang);
            }
            // If it's too late, continue straight away without sleeping
        }
    }

    jalankan_pada_interval(Duration::from_millis(50), 5);
}

fn duration_from_millis_sat(ms: u64) -> Duration {
    Duration::from_millis(ms)
}

Summary #

  • Duration for interval, Instant for measurement, SystemTime for calendar — all three have different roles. Mixing them up is a common source of bugs.
  • Use Instant::now() to measure performance — it is monotonic and not affected by system clock changes. elapsed() always works without unwrap.
  • SystemTime can reverseduration_since returns Result instead of Duration because the system time can be reset. Always handle the error.
  • Duration::checked_sub and saturating_sub — avoid panic when Duration arithmetic operations can produce negative values. saturating_sub returns ZERO instead of panic.
  • recv_timeout and try_lock for timeout — use Duration as the timeout parameter for blocking operations such as channel recv and Mutex lock.
  • thread::sleep not precise — OS could delay longer than requested. For real-time applications, use async runtime with more precise timers, or spin-wait for microsecond granularity.
  • Use chrono for calendar dates — correct formatting, parsing, timezone, and month/year arithmetic. The standard library deliberately does not include it due to the complexity of timezones.
  • Loop interval without drift — use pattern deadline += interval instead of sleep(interval). The second one will drift because the code execution time is not zero.


← Previous: Sync   Next: Process →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact