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:#e8f5e9SystemTime — 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 #
Durationfor interval,Instantfor measurement,SystemTimefor 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 withoutunwrap.SystemTimecan reverse —duration_sincereturnsResultinstead ofDurationbecause the system time can be reset. Always handle the error.Duration::checked_subandsaturating_sub— avoid panic when Duration arithmetic operations can produce negative values.saturating_subreturnsZEROinstead of panic.recv_timeoutandtry_lockfor timeout — useDurationas the timeout parameter for blocking operations such as channel recv and Mutex lock.thread::sleepnot 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
chronofor 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 += intervalinstead ofsleep(interval). The second one will drift because the code execution time is not zero.