skip to content

You are deciding how an application should persist timestamps when its users span many time zones. Given that a JavaScript Date models only a UTC instant, when is that the right representation and when is it provably the wrong one?

level: principalimportance: should knowfreq 31%

answer

  1. instants, wall times, and plain dates differ
  2. past facts are instants
  3. future local commitments are not
  4. zone rules change by legislation
  5. resolve late, store the intent

basics

~20 s

An instant is right for things that happened: creation times, log entries, expiries, anything ordered on a timeline. It is wrong for future wall-clock commitments and for calendar dates with no time, because converting those to an instant bakes in a time-zone rule that can change or a zone that was never meant to apply.

solid answer

~50 s

A JavaScript `Date` can represent exactly one thing: an instant on the timeline, stored as milliseconds since the epoch. That is the correct model for anything in the past or anything whose meaning is "a moment" — `created_at`, audit records, token expiry, event ordering — and for those, storing UTC and converting only at the display edge is the durable rule. It is the wrong model in two cases. First, a future wall-clock commitment: an alarm at 07:00 or a meeting at 09:00 next March means a local reading, and pinning it to an instant now freezes the zone's current daylight-saving rules, which governments change with a few months' notice. Second, a pure calendar date — a birthday, an invoice period, a public holiday — which has no instant at all; forcing it through a Date makes it read as the neighbouring day for half the world. Those want wall time plus an IANA zone identifier, or a plain date value, resolved late.

go deeper

for a junior

Know that a Date holds an instant in UTC and that storing and transmitting instants in UTC, converting only when displaying, is the baseline rule for anything already recorded.

for a middle

Distinguish the three kinds of time value — instant, wall time in a zone, plain calendar date — and explain why a date-only value should never be turned into a Date at all.

for a senior

Show where the boundaries go in a real system: which columns hold instants, where conversion is allowed to happen, and how you keep a picked calendar date from shifting a day as it round-trips through storage.

for a principal

Own the policy: what each stored time value means, why future commitments resolve late against an updatable time-zone database, which zone defines a reporting day, and how the representation itself prevents the whole bug class rather than each instance.

## Three distinct kinds of time value The design question is not "UTC or local" — it is "which of three things is this value?" 1. **An instant.** A point on the physical timeline, the same moment for everyone. "The order was placed." A `Date` models this exactly. 2. **A wall-clock time in a place.** A local reading that resolves to an instant only once you know the zone's rules at that date. "The standup is at 09:00 in Berlin." 3. **A calendar date.** A day with no time and no zone. "Her birthday is 14 March." No instant is implied at all. JavaScript's `Date` implements only the first. Everything painful about dates in JavaScript comes from storing the other two kinds inside it. ## When the instant is right For recorded facts, an instant is not just adequate — it is the only defensible choice. Events that already happened have a definite moment; they need to sort correctly, compare across services running in different zones, and survive a server being reconfigured. Store the UTC instant, transmit it as an ISO 8601 string with `Z` or an explicit offset, and convert only when rendering. The corollary is boundary discipline. The moment a local reading enters storage or a message, information is lost that no downstream consumer can recover. Two rules follow: nothing crosses a process boundary without an explicit offset, and no persistence path uses the local-field constructor or the unprefixed getters. Durations and expiries belong here too. A 24-hour token lifetime is elapsed time; computing it as instant plus milliseconds is correct precisely because it ignores the calendar. ## When the instant is provably wrong **Future wall-clock commitments.** Converting "09:00 on 15 March next year in Chicago" into a UTC instant requires knowing Chicago's offset on that date. You can only apply *today's* rules. Time-zone rules are political: jurisdictions change transition dates, abolish daylight saving, or shift their standard offset, often with months rather than years of notice, and the IANA time-zone database is updated several times a year to track it. If a rule changes between storing and firing, the stored instant now maps to 08:00 or 10:00. The value that stays correct is the wall time plus the zone identifier — `{ local: '2026-03-15T09:00', zone: 'America/Chicago' }` — resolved to an instant when the occurrence is due. Recurring events are the acute case, because a single stored rule expands to occurrences on both sides of a transition. There is a genuine tradeoff: querying "what fires in the next hour" over stored wall times is harder than over instants. The usual resolution is a hybrid — store the wall time and zone as the source of truth, and materialise a short horizon of resolved instants as a derived index that you can safely recompute when the time-zone database updates. **Calendar dates.** A birthday or an invoice date has no instant. Storing it as a Date forces a fictitious time — usually midnight — in some zone, and every reader west or east of that zone sees the neighbouring day. This produces the most-reported date bug in web applications: a date picked as 15 March persists, comes back, and displays as 14 March. The fix is representational: keep the value as `'2025-03-15'` — a string or three integer fields — end to end, and never let it become a Date. Any layer that converts it to an instant for convenience reintroduces the bug. **Ambiguous cases worth naming.** A birth *timestamp* in a medical record is an instant; a birth *date* on a form is a calendar date. A business day boundary for reporting is a wall-clock rule in a specified zone, not an instant, and "today's sales" means different windows in different reporting zones — that must be a stated policy, not an accident of where the server runs. ## Consequences for the codebase - **Type the difference.** If instants and calendar dates are both "a Date", the distinction lives only in developers' heads. Give calendar dates their own representation so the compiler, the schema, or at minimum the naming enforces it. - **Name the columns and fields for what they hold.** `occurred_at` for an instant; `service_date` and `service_zone` for a wall-clock commitment; `birth_date` for a calendar date. - **Put conversion in one place.** A single rendering layer converts instants to local readings; nothing else calls the local getters. - **Decide the reporting zone explicitly.** "Which day did this belong to" is a policy question — the user's zone, the merchant's zone, or UTC — and it must be written down, not inherited from the host's configuration. - **Test in a non-UTC zone.** Running everything in UTC containers makes every one of these bugs invisible until a user finds it. ## The judgment to demonstrate The strong answer is not "always store UTC". It is: *store the kind of value you actually have.* UTC instants for facts on the timeline, wall time plus zone for future human commitments, plain dates for calendar values — with late resolution wherever a political rule change could intervene between writing and reading.

  • Why does storing a UTC instant for a future recurring meeting fail, when the same approach is perfectly safe for a log entry?
    A log entry records a moment that already happened; its instant can never become wrong. A future meeting is a wall-clock promise, and converting it to an instant applies today's time-zone rules to a date those rules may not govern by the time it arrives. Governments change daylight-saving rules with a few months' notice, and the stored instant then maps to the wrong local hour.
  • A user picks 15 March in a date field and later sees 14 March. What is the structural fix, as opposed to a patch?
    Stop representing the value as an instant. A calendar date has no time and no zone, so any conversion picks a fictitious midnight in some zone and every reader in a different zone sees the neighbour. Keep it as '2025-03-15' or as year/month/day fields through the whole path — storage, transport and rendering — so no conversion ever happens.
  • If wall-time-plus-zone is the correct storage for future events, how do you still query efficiently for what fires in the next hour?
    Keep the wall time and zone identifier as the source of truth, and materialise a short horizon of resolved instants as a derived index for querying. Because it is derived, you can recompute it whenever the time-zone database is updated, so a rule change corrects the schedule instead of silently corrupting it.
  • How do you decide which time zone defines "today" for a daily report?
    It is a product policy, not a technical default: the user's zone, the merchant's zone, or UTC are all defensible, and they give different numbers for the same data. The failure mode is leaving it implicit, so the answer becomes whatever zone the server happens to be configured with — and changes when the service is redeployed.

saying these in an interview costs you the question

  • Says store everything as UTC and stop thinking
  • Treats a birthday as a timestamp at midnight
  • Assumes time-zone offsets are fixed properties of a place
  • Stores a resolved instant for events years ahead
  • Lets the server's configured zone define the reporting day

context