How to convert between timestamps and dates
Short answer: the epoch is a constant in the standard library, UNIX_EPOCH, and every conversion goes through it. A timestamp is just the Duration between that constant and the moment you care about — and a Duration turns back into a moment by being added to the same constant:
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); let secs = now.duration_since(UNIX_EPOCH).unwrap().as_secs(); println!("seconds since epoch: {secs}");
let back = UNIX_EPOCH + Duration::from_secs(secs); let round = back.duration_since(UNIX_EPOCH).unwrap().as_secs(); println!("back again: {round}"); println!("same: {}", round == secs);}cargo run -qseconds since epoch: 1793212317back again: 1793212317same: trueYour first number will be larger — it counts every second since 1970-01-01 UTC, and those keep accumulating. The shape is what matters: duration_since(UNIX_EPOCH) goes from moment to number, UNIX_EPOCH + Duration::from_secs(...) goes from number back to moment, and as_secs() is where the Duration becomes a plain integer you can store in a database or send over the network. From a fresh project — cargo new timestamp-demo, then replace src/main.rs with each block below.
The two that look alike
Section titled “The two that look alike”Timestamps arrive in two units. Seconds since the epoch is ten digits today; milliseconds since the epoch is thirteen. JavaScript’s Date.now() hands out milliseconds, most APIs that grew out of JavaScript do the same, and a 10-digit and a 13-digit number both look like “a timestamp” to anyone in a hurry.
Rust will not catch the mix-up. Duration::from_secs and Duration::from_millis both compile, and the result is a valid SystemTime either way — just wrong by a factor of a thousand:
use std::time::{Duration, UNIX_EPOCH};
fn main() { let ts: u64 = 1_793_000_000;
let as_seconds = UNIX_EPOCH + Duration::from_secs(ts); let as_millis = UNIX_EPOCH + Duration::from_millis(ts); println!("treated as seconds: {} s", as_seconds.duration_since(UNIX_EPOCH).unwrap().as_secs()); println!("treated as milliseconds: {} s", as_millis.duration_since(UNIX_EPOCH).unwrap().as_secs());}treated as seconds: 1793000000 streated as milliseconds: 1793000 sThe same number, read in the wrong unit, lands in late January 1970 instead of late 2026. There is no runtime error to notice — the date is simply half a century off, which is why a test that asserts on a converted date earns its keep. When a timestamp’s origin is unknown, the digit count is the tell: ten digits is seconds, thirteen is milliseconds.
Every conversion is a truncation
Section titled “Every conversion is a truncation”Converting between the two units loses the tail, and Duration has the same knife built in:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let js_ms: u64 = 1_793_000_000_123; println!("ms to s: {}", js_ms / 1000);
let now = SystemTime::now(); let d = now.duration_since(UNIX_EPOCH).unwrap(); println!("as_secs: {}", d.as_secs()); println!("as_millis: {}", d.as_millis()); println!("as_secs dropped: {} ms", d.as_millis() % 1000);}ms to s: 1793000000as_secs: 1793212324as_millis: 1793212324172as_secs dropped: 172 ms(The dropped amount is whatever fraction of a second the clock was in — it varies on every run, between 0 and 999 ms.)
Two truncations in one block. js_ms / 1000 is integer division: the 123 at the end of the millisecond timestamp is discarded, so a “this request arrived at” comparison that used to resolve to the millisecond now resolves to the second. as_secs() does the same thing to the clock itself — it chops everything below one second, which as_millis() % 1000 makes visible. Neither is a bug; both are the honest answer to “give me this in whole seconds”. Just know which precision you threw away: keep as_millis() if sub-second ordering matters, use as_secs() only when whole seconds are the contract.
Timestamps before 1970
Section titled “Timestamps before 1970”A Duration cannot be negative, so the moment-to-number direction has a blind spot: anything before the epoch. The code still runs, but the answer changes shape:
use std::time::{Duration, UNIX_EPOCH};
fn main() { let before = UNIX_EPOCH - Duration::from_secs(86_400); match before.duration_since(UNIX_EPOCH) { Ok(d) => println!("after the epoch: {} s", d.as_secs()), Err(e) => println!("error: {e}"), }
let too_big = UNIX_EPOCH.checked_add(Duration::from_secs(u64::MAX)); println!("checked add u64::MAX: {too_big:?}");}error: second time provided was later than selfchecked add u64::MAX: NoneThe Err is the before-1970 case: duration_since refuses to invent a negative Duration, so it reports that its argument is later than self — a confusing message until you read it as “the result would be negative”. If a timestamp can legitimately be negative (birth dates of anyone born before 1970, historical records), a u64 of seconds since the epoch is the wrong representation, and this is where a date library earns its keep. The None is the same story at the far end: even u64 seconds overflow the platform’s internal clock representation eventually, and checked_add tells you instead of panicking.
Where the actual date is
Section titled “Where the actual date is”None of this turns 1793212219 into “2026-10-29”. The standard library has the epoch and the arithmetic, but no calendar: there is no year, month, or day accessor on SystemTime, and no way to format one. Building a timestamp from a calendar date, or printing one as one, needs a date library — parsing and formatting dates with chrono covers that half of the exchange.
- The previous article — why measuring durations belongs to
Instant, not the clock that produces these timestamps. - Working with dates and time — the rest of the chapter.