Skip to content
Select theme

How to work with timezones

Short answer: the standard library has one timezone, and it is UTC. SystemTime::now() counts seconds since 1970-01-01 UTC no matter where the machine sits, and there is no local-time API anywhere in std. Working with timezones in std-only Rust is therefore two moves: store everything in UTC, and leave the conversion to whatever sits at the edge of your program. This page shows the proof that the TZ environment variable really has no effect, what a timezone actually changes, and the DST trap that breaks 86,400-second day arithmetic.

From a fresh project — cargo new tz-demo, then replace src/main.rs:

Now run it twice, once claiming to be in Tokyo and once in New York:

Terminal window
TZ=Asia/Tokyo cargo run -q
TZ=America/New_York cargo run -q
timestamp: 1793000000
timestamp: 1793000000

(That is the combined output of the two runs — one line each.)

The TZ variable is what every tool around Rust consults: date, ls -l, most log libraries read it to decide how to print times. Rust’s standard library never reads it. The same timestamp prints the same number in Tokyo and in New York, because the number does not describe a wall clock — it describes an instant. This is a design decision, not an oversight: a “local time” needs a timezone database (which offset applies here, when daylight saving switches, all of it subject to change by law), and std does not ship one. So the clock it hands you is UTC by construction.

A timezone is two things: an offset from UTC, plus a set of daylight-saving rules. The instant stays the same; the wall clock moves. Your shell already knows the rules, so the fastest way to see the gap is to ask it about the same instant — date -r N takes epoch seconds on macOS and BSD:

Terminal window
TZ=America/New_York date -r 1772884800
TZ=Asia/Tokyo date -r 1772884800
TZ=Europe/Berlin date -r 1772884800
Sat Mar 7 07:00:00 EST 2026
Sat Mar 7 21:00:00 JST 2026
Sat Mar 7 13:00:00 CET 2026

One instant — 1772884800 is 2026-03-07 12:00 UTC — three wall clocks. New York is 5 hours behind UTC, Tokyo 9 ahead, Berlin 1 ahead. Nothing in std can produce any of those three lines, and that is exactly the division of labor the design pushes you toward: instants and arithmetic belong to the standard library; turning an instant into a wall clock belongs to the display layer, which is where the timezone database lives.

Adding 86,400 seconds is the natural way to write “this time tomorrow”, and on most days it is even right. The days it is wrong are the DST transition days. In the United States, clocks jump forward at 2 a.m. local time on the second Sunday of March — in 2026, that is March 8, and New York moves from EST (UTC−5) to EDT (UTC−4):

86,400 s later: 1772971200
same 7 am again: 1772967600
difference: 3600 s

The shell confirms what those numbers mean in New York:

Terminal window
TZ=America/New_York date -r 1772884800
TZ=America/New_York date -r 1772971200
TZ=America/New_York date -r 1772967600
Sat Mar 7 07:00:00 EST 2026
Sun Mar 8 08:00:00 EDT 2026
Sun Mar 8 07:00:00 EDT 2026

Read the three lines together. sat_morning + 86_400 s is the same instant one day later, and one day later that instant reads 08:00 — New York sprang forward overnight, so the wall clock slid an hour. The instant that does read 07:00 the next morning is only 82,800 seconds away, and the 3,600-second gap between the two is exactly the hour the clock skipped. std does the instant side of this perfectly — arithmetic on SystemTime cannot be corrupted by DST, because DST never touches it. What it cannot do is the wall-clock side: deciding “when is 7 a.m. in New York” needs the rules, and that is a date library’s job.

Two footnotes before you pick a habit: Europe switches on a different schedule (last Sunday of March — 2026-03-29), so “the DST day” is not one global date; and some zones have no DST at all — China stays at UTC+8 all year, which is why 86,400-second arithmetic never breaks there.

The pattern: UTC everywhere, convert at the edge

Section titled “The pattern: UTC everywhere, convert at the edge”

The design of std is also the convention worth copying:

  • Store instants. A u64 of epoch seconds (or SystemTime itself) is timezone-free by construction — the value cannot be misread by a machine in another zone.
  • Do arithmetic in UTC. Expiry checks, ordering, durations: all instant math, all DST-proof.
  • Convert only at the edge. The moment a time is displayed, entered by a user, or scheduled against a wall clock, a timezone enters the picture — and at that point you need the rules, which std does not have. chrono’s local time and formatting picks up there, reading the same zone database the shell’s date just did.