Unix Timestamp — Off-by-One Day in UTC vs Local

webdev unix-timestamp timezone datetime tools

Support says the webinar is “on the 15th.” The database says 1718496000. In Tokyo it formats as the 16th. In New York it is still the 15th evening. Nobody changed the event — someone mixed instant storage with calendar language.

Timestamps are instants, not wall clocks

Unix time counts elapsed time since the epoch in UTC. It does not know “Japan” or “Eastern.” When you print it, you choose a zone:

const ts = 1718496000; // seconds
new Date(ts * 1000).toISOString();           // always UTC
new Date(ts * 1000).toString();              // local machine zone

The bug is rarely “Unix is wrong.” The bug is parsing "2024-06-16" as local midnight, converting to epoch, then displaying in another zone.

Seconds vs milliseconds

SourceTypical unitDigit length (2020s)
Many APIs / PHPseconds10
JavaScript Date.now()milliseconds13
Some Java/Android APIsmilliseconds13
// Wrong: treat ms as seconds → year ~56000
new Date(Date.now()).toISOString(); // accidental if you forgot Date.now is already ms

// Wrong: seconds passed to Date as ms → 1970 + a few weeks
new Date(1718496000).toISOString();

When debugging, convert explicitly and label the unit in logs: ts_sec vs ts_ms.

The midnight trap

Product asks: “Store the user’s selected day.” That is a civil date, not an instant. Options:

  1. Store YYYY-MM-DD as a date type (Postgres date) and never convert through local midnight.
  2. Store an instant that means “start of day in zone X” and always carry the zone name (America/New_York).
  3. Store UTC midnight of that date only if everyone agrees the date is a UTC date (rare for consumer UX).
User in US picks June 16
→ wrong: Date.parse("2024-06-16") in a US browser (local midnight)
→ send epoch to API
→ Japan admin UI formats in JST → June 16 afternoon or June 17

Same epoch, different calendar labels. Align on whether the business cares about a day-in-timezone or an absolute meeting time.

A debugging sequence that converges

  1. Print the raw number and count digits (sec vs ms).
  2. Convert with a Unix timestamp converter showing UTC and a chosen local zone side by side.
  3. Check the writer: did it use getTimezoneOffset(), toISOString(), or a library with an explicit IANA zone?
  4. Check the reader: is the UI using toLocaleString without options, or Intl with timeZone set?
  5. Fix one layer — prefer storing UTC instants for events, and civil dates for birthday-like fields.

Library notes without dogma

  • Luxon / Temporal (where available) — force you to name zones; fewer silent local assumptions.
  • Bare Date — fine if every call site is reviewed for UTC vs local methods (getUTC* vs get*).
  • Cron and “run at midnight” — always specify the zone in the scheduler; “server local” changes when you move regions.

What to put in the API contract

Document one of:

  • event_at: RFC 3339 string with Z or offset
  • event_at_ms: integer epoch milliseconds UTC
  • calendar_day: YYYY-MM-DD plus timezone for interpretation

Ambiguous "2024-06-16T00:00:00" without offset is how off-by-one-day tickets are born. Name the unit, name the zone at the boundary, and use a converter when two systems disagree — the math is simple once you stop treating timestamps as “dates with timezones baked in.”