Unix Timestamp — Off-by-One Day in UTC vs Local
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
| Source | Typical unit | Digit length (2020s) |
|---|---|---|
| Many APIs / PHP | seconds | 10 |
JavaScript Date.now() | milliseconds | 13 |
| Some Java/Android APIs | milliseconds | 13 |
// 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:
- Store
YYYY-MM-DDas a date type (Postgresdate) and never convert through local midnight. - Store an instant that means “start of day in zone X” and always carry the zone name (
America/New_York). - 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
- Print the raw number and count digits (sec vs ms).
- Convert with a Unix timestamp converter showing UTC and a chosen local zone side by side.
- Check the writer: did it use
getTimezoneOffset(),toISOString(), or a library with an explicit IANA zone? - Check the reader: is the UI using
toLocaleStringwithout options, orIntlwithtimeZoneset? - 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*vsget*). - 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 withZor offsetevent_at_ms: integer epoch milliseconds UTCcalendar_day:YYYY-MM-DDplustimezonefor 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.”