The rules
Every “always” and “never” in this guide, distilled into one deep-linkable list — share a rule by its link.
This guide spends its pages on why. This one is the what to do: the guide’s recurring edicts, stripped to imperatives and collected in one place. Every rule’s heading is a link to itself — click one and copy the address to cite exactly that rule in a code review or a design doc. Each rule also links to the page that earns it, including the cases where it bends.
The pitfalls index is this page’s negative mirror: what it looks like when these rules are broken, with the incidents to prove it.
Storing & modeling
Store instants in UTC
An instant — something that happened or will fire at a point on the timeline — is stored in UTC, as RFC 3339 text or a database instant type, and converted to a zone only at the edges. → Store vs display
Give civil values a civil type
A birthday, an invoice date, a “9 AM” preference has no zone and names no instant. Storing one as a UTC timestamp invents a time and a zone it never had, and can shift the date by a day. → Store vs display, What is a date?
Keep the zone next to anything you must re-localize
An offset pins an instant; only the IANA zone name carries the rules for
future readings. If a value must come back as a wall-clock time — an event
venue, a user preference — store America/New_York with it, not -05:00.
→ Zones vs offsets
Store recurrences as a rule, not occurrences
“Every day at 9 AM” is a civil rule plus a zone plus exceptions. Materialized UTC instants drift at every DST transition and break when zone rules change; expand the rule as late as possible. → Recurring events
Make the type say what the value means
The schema is the one piece of documentation the database enforces. A civil
date in a timestamptz, or an instant in a bare string, tells every other
consumer a lie. → Your schema is a contract
Name fields with kind, basis, and units
created_at for an instant, due_on for a date, timeout_ms for a
duration. The name is read by people who will never see your code — make it
answer what the type cannot. → Naming time fields
Exchanging
Document the wire format
The core interchange rule is not “use format X” — it is that both ends know, in writing, exactly which format is in play. RFC 3339 in UTC is the safe default to document. → Your own client ↔ server, RFC 3339
Never emit a timestamp without an offset
2026-06-05T14:30:00 forces every consumer to guess the zone, and each will
guess differently. Always Z or an explicit ±hh:mm.
→ RFC 3339
Treat third-party timestamps as untrusted input
Verify the format, the unit (seconds vs milliseconds), and the basis before the first value crosses into your system — APIs rarely label any of them, and they change without notice. → 3rd-party data
Never parse human input leniently
Free-text dates are locale-dependent and ambiguous; a lenient parser silently guesses. Parse strictly against known formats, use the user’s locale, and ask when it is still ambiguous. → Parsing user input
Displaying
Format late, with an explicit zone and locale
Formatting is presentation: keep values precise until the rendering layer, then hand them to a locale-aware formatter with the zone and locale supplied deliberately — never inherited silently from the environment. → Formatting for display
Never hand-assemble a date string for a person
Concatenating day, month, and year yourself hard-codes one locale’s order and punctuation and gets pluralization, month names, and right-to-left scripts wrong. Delegate to the platform’s formatter. → Formatting for display
Computing
Measure elapsed time with a monotonic clock
The wall clock jumps — NTP corrections, DST, a user changing the time. For durations, timeouts, and benchmarks, use the clock that only moves forward. → Clocks
Do calendar math in civil time, through a zone
“Tomorrow” and “next month” are civil steps, not fixed numbers of seconds. Adding 86 400 seconds lands an hour off at every DST transition; step the calendar date in the zone instead. → DST, Durations
Never assume a day has 24 hours
DST gives some local days 23 and some 25; gaps mean some local times never exist, and overlaps mean some happen twice. Code that assumes otherwise breaks exactly twice a year. → DST