When time arithmetic gets tricky
You are building a rate limiter for an API client. You store the timestamp of the last request, add the cooldown duration, and compare the result against the current time. In Python, you might write last_request + timedelta(seconds=5). In JavaScript, you add milliseconds to a Date object. In Rust, the compiler rejects your code before it runs. There is no + operator for time.
Rust splits time into distinct types to prevent silent bugs. A span of time is not the same as a point in time. A wall-clock timestamp can jump backwards due to system adjustments, while a monotonic timer never goes backward. Mixing these concepts requires explicit methods, not operators. This design forces you to handle overflow, clock jumps, and calendar quirks at the call site. You cannot accidentally subtract a duration from a timestamp and get a nonsense value. The type system blocks the mistake.
Duration: the span of time
Duration represents a span of time. It is a struct containing seconds and nanoseconds. You use Duration for timeouts, intervals, and elapsed time. It lives in std::time.
Duration supports arithmetic with other Duration values. You can add and subtract them using the + and - operators. The + operator is safe and never panics. The - operator panics if the result would be negative. This panic is intentional. A negative duration has no meaning in Rust's model. If you need to handle cases where the second duration is larger, use checked_sub, which returns Option<Duration>.
use std::time::Duration;
fn main() {
// Duration stores seconds as u64 and nanoseconds as u32.
// Arithmetic handles the carry between seconds and nanoseconds automatically.
let five_seconds = Duration::from_secs(5);
let two_seconds = Duration::from_secs(2);
// Addition is safe and always succeeds.
let total = five_seconds + two_seconds;
println!("Total: {:?}", total);
// Subtraction panics if the result is negative.
// let negative = two_seconds - five_seconds; // PANIC: attempt to subtract with overflow
// Use checked_sub to handle negative results gracefully.
let safe_diff = two_seconds.checked_sub(five_seconds);
match safe_diff {
Some(d) => println!("Difference: {:?}", d),
None => println!("Second duration is larger; result would be negative."),
}
}
Convention aside: prefer Duration::from_secs and Duration::from_millis over Duration::new unless you need nanosecond precision. Duration::new takes both seconds and nanoseconds, which makes the call site verbose. The from_* constructors are clearer and the compiler optimizes them identically. If you are converting from floating-point seconds, use Duration::from_secs_f64, but be aware that floating-point arithmetic loses precision. For high-precision timing, stick to integer-based constructors.
Duration has a maximum value. Adding two large durations can overflow. The + operator wraps on overflow in debug builds and panics in release builds? No, Duration addition saturates or wraps depending on the implementation? Actually, Duration implements Add using checked arithmetic that panics on overflow in debug mode and wraps in release mode? Let's verify. Duration::checked_add exists. The + operator on Duration uses checked_add and panics on overflow. This is consistent with Rust's arithmetic rules for checked operations. If you need non-panicking addition, use checked_add.
Don't assume durations are infinite. Use checked_add when combining user-provided values. The compiler protects you from overflow, but only if you ask for it.
SystemTime: the wall clock
SystemTime represents a point in time on the system clock. It corresponds to the wall clock, like the time shown in the corner of your screen. You use SystemTime for logging, scheduling, and storing timestamps in databases.
SystemTime does not implement the Add or Sub traits. You cannot write time + duration. Instead, you must use checked_add and checked_sub. These methods return Option<SystemTime> and Option<Duration> respectively. The return type is Option because the operation can fail. The system clock has limits. Adding a duration might exceed the maximum representable time. Subtracting might go before the minimum. checked_add returns None in these cases.
use std::time::{Duration, SystemTime};
fn main() {
let now = SystemTime::now();
let five_minutes = Duration::from_secs(300);
// SystemTime does not support the + operator.
// let future = now + five_minutes; // ERROR: no implementation for `SystemTime + Duration`
// Use checked_add to add a duration to a timestamp.
let future = now.checked_add(five_minutes).expect("Time overflow: duration too large");
println!("Future time: {:?}", future);
// Subtraction returns a Result because the difference might be negative or invalid.
// checked_sub returns Option<Duration>.
let diff = future.checked_sub(now).expect("Times are too far apart");
println!("Difference: {:?}", diff);
}
SystemTime can jump. The operating system adjusts the clock periodically using NTP (Network Time Protocol). The clock can move forward or backward. If you measure elapsed time using SystemTime, your measurement might be wrong. The clock might have been adjusted between your start and end readings. Never use SystemTime to measure elapsed time or for timeouts. Use Instant for that.
SystemTime subtraction returns a Result in the sub method, but checked_sub returns Option. The Result variant includes an error type that explains why the subtraction failed. In practice, checked_sub is easier to work with. It returns None if the subtraction is invalid.
The wall clock lies to you during NTP adjustments. Use SystemTime for human-readable timestamps, not for measuring intervals.
Instant: the stopwatch
Instant represents a point in time on a monotonic clock. It is like a stopwatch. The clock starts at some arbitrary point and only moves forward. It never jumps backward. You use Instant for measuring elapsed time, setting timeouts, and calculating performance metrics.
Instant supports checked_add and checked_sub. It also provides duration_since, which returns the time elapsed since the instant. duration_since returns a Result because the other instant might be in the future. If the argument is in the future, duration_since returns an error.
use std::time::{Duration, Instant};
fn main() {
let start = Instant::now();
// Simulate some work.
std::thread::sleep(Duration::from_millis(100));
let end = Instant::now();
// duration_since returns the elapsed time.
// It returns a Result because the argument might be in the future.
let elapsed = end.duration_since(start).expect("Time went backwards");
println!("Elapsed: {:?}", elapsed);
// Instant supports checked_add for calculating deadlines.
let deadline = start.checked_add(Duration::from_secs(10)).expect("Deadline overflow");
println!("Deadline: {:?}", deadline);
}
Convention aside: use Instant::now() for timers and SystemTime::now() for logs. This distinction is a community standard. Code reviewers will flag SystemTime usage in performance-critical paths. Instant is monotonic and immune to clock adjustments. SystemTime is subject to NTP jumps. Mixing them leads to subtle bugs where a timeout fires early or late because the clock shifted.
Instant values are not comparable across processes. An Instant from one process has no meaning in another. Do not serialize Instant and expect it to work elsewhere. Use SystemTime for timestamps that need to be shared or stored.
The stopwatch never lies. Pick Instant for timers, SystemTime for logs.
Calendar math requires chrono
Duration and SystemTime handle seconds and nanoseconds. They cannot handle months, years, or leap days. Adding one month to a date is ambiguous. Does it mean 30 days? 31 days? The next month regardless of length? Rust's standard library does not guess. It leaves calendar arithmetic to crates.
The chrono crate provides types for calendar dates and times. It supports adding months, years, and handling timezones. If you need to calculate a birthday, a billing cycle, or a date range, use chrono.
// This code requires the chrono crate.
// Add `chrono = "0.4"` to Cargo.toml.
use chrono::{DateTime, Duration as ChronoDuration, Utc};
fn main() {
let now: DateTime<Utc> = Utc::now();
// chrono::Duration supports months and years.
let one_month = ChronoDuration::months(1);
let next_month = now + one_month;
println!("Now: {}", now);
println!("Next month: {}", next_month);
}
chrono uses its own Duration type, which is distinct from std::time::Duration. chrono::Duration can represent months and years. std::time::Duration cannot. The two types do not mix. Convert between them when crossing the boundary.
Calendar math belongs in chrono. Duration stops at nanoseconds.
Pitfalls and compiler errors
Time arithmetic in Rust exposes several common pitfalls. The compiler catches many of them, but some require careful handling.
If you try to add a Duration to a SystemTime using the + operator, the compiler rejects you with E0277 (trait bound not satisfied). The error message says there is no implementation for SystemTime + Duration. This is a feature. The compiler forces you to use checked_add, which makes you think about overflow.
use std::time::{Duration, SystemTime};
fn main() {
let now = SystemTime::now();
let dur = Duration::from_secs(10);
// ERROR: E0277: the trait bound `SystemTime: std::ops::Add<Duration>` is not satisfied
let future = now + dur;
}
If you subtract two Duration values and the result is negative, the program panics. This panic happens at runtime. Use checked_sub to avoid it. The panic message is clear: "attempt to subtract with overflow". In production code, panics are undesirable. Handle the None case from checked_sub.
SystemTime subtraction can fail if the times are too far apart. The sub method returns a Result. The checked_sub method returns Option. If you ignore the Result or Option, you risk a panic or a logic error. Always handle the failure case.
SystemTime can go backwards. If you store a timestamp and compare it later, the comparison might be wrong if the clock was adjusted. Use Instant for comparisons that depend on elapsed time.
Don't guess at time arithmetic. Use checked_add and handle the None. The compiler protects you from overflow, but only if you ask for it.
Decision: choosing the right type
Rust provides multiple types for time. Choosing the right one depends on your use case. Use the parallel structure below to decide.
Use Duration when you need a span of time, such as a timeout, interval, or elapsed time. Use Duration for arithmetic between spans. Use checked_add and checked_sub to handle overflow and negative results.
Use SystemTime when you need a wall-clock timestamp, such as for logging, storing in a database, or displaying to a user. Use checked_add and checked_sub to add or subtract durations. Handle the Option return value.
Use Instant when you need to measure elapsed time, set a timeout, or calculate performance metrics. Use Instant for monotonic timing that is immune to clock adjustments. Use duration_since to get the elapsed time.
Use chrono when you need calendar arithmetic, such as adding months, years, or handling leap days. Use chrono for timezones and date formatting. Convert to std::time types when interacting with the standard library.
Use std::time::SystemTime for interoperability with OS APIs and file metadata. Use chrono::DateTime for human-readable dates and times.
The compiler forces you to choose the right tool. Pick Instant for timers, SystemTime for logs, Duration for spans, and chrono for calendars.