Skip to content
Select theme

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:

Terminal window
cargo run -q
seconds since epoch: 1793212317
back again: 1793212317
same: true

Your 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.

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:

treated as seconds: 1793000000 s
treated as milliseconds: 1793000 s

The 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.

Converting between the two units loses the tail, and Duration has the same knife built in:

ms to s: 1793000000
as_secs: 1793212324
as_millis: 1793212324172
as_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.

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:

error: second time provided was later than self
checked add u64::MAX: None

The 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.

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.