Why your date is off by one day: time zones and DST

Most date bugs are not arithmetic errors. They come from mixing two different things — a moment in time and a label on a calendar — and from assuming a day is always 24 hours long.

Two kinds of “date”

An instant is a point on the global timeline, independent of where you are: the moment a payment cleared, a log line was written, a token expired. A wall-clock date is a label people use — “2026-03-08”, “next Tuesday”, “the end of the month” — and its meaning depends on the time zone in which it is read. A Unix timestamp stores an instant. A date input stores a label. Bugs happen when one is treated as the other.

The classic symptom is a date that shifts by a day when it crosses a boundary. A value entered as 2026-01-01, stored as UTC midnight, and displayed in New York (UTC−5) is 2025-12-31 19:00 — the previous day. Nothing was computed incorrectly; the same instant was simply labelled in a different zone. The fix is a convention: decide whether a value is an instant or a calendar date, and keep it in that form all the way through.

Unix timestamps are instants

A Unix timestamp counts seconds since 1970-01-01T00:00:00Z — the trailing Z means UTC, and that is the whole point: the number is the same everywhere on Earth. Seconds versus milliseconds is the first trap; JavaScript's Date.now() is milliseconds, most server logs and Unix tools use seconds, and the difference is a factor of 1,000. A 13-digit number is milliseconds, a 10-digit number is seconds, and feeding one where the other is expected puts the date decades away.

InputResultNote
1760000000 (seconds)2025-10-09T08:53:20ZAn instant; the same in every zone
1760000000000 (milliseconds)2025-10-09T08:53:20ZThe same instant, 1,000× the number
“2026-01-01” (bare date)No instantA label until a time and zone are chosen

The first two rows are the output of the timestamp converter on this site; the third is the case that causes most of the confusion.

Bare dates are ambiguous on purpose

JavaScript's new Date('2026-01-01') parses a date-only string as UTC midnight, while new Date(2026, 0, 1) constructs local midnight. Both are “2026-01-01” and they are different instants whenever the local offset is not zero. The same ambiguity exists in databases, JSON APIs and date pickers: a column typed DATE usually has no zone, a column typed TIMESTAMP usually does, and an API that sends "2026-01-01" is not telling you which it meant.

The defensible convention for a calendar date is to keep it as a calendar date — a year, month and day — and never convert it to an instant until you know the zone that gives it meaning. For a birthday, a holiday or an invoice date, that zone is the user's; for a contract deadline, it should be written in the contract, not inferred from the server's locale.

A day is not always 24 hours

Calendar arithmetic and elapsed time diverge twice a year in every country that observes daylight saving. In America/New_York, the local day that starts on 2026-03-07 ends 47 hours later on 2026-03-09, because 8 March is 23 hours long; the autumn change makes 1 November 25 hours, so 31 October to 2 November is 49 hours. Both spans are “two days” on the calendar and neither is 48 hours of elapsed time.

Local span (America/New_York)Calendar daysElapsed hours
2026-03-07 → 2026-03-09247
2026-10-31 → 2026-11-02249
2026-01-15 → 2026-07-151814,343 (offset changes from −5 to −4)

Measured in Node with TZ=America/New_York; the offsets are 300 minutes in January and 240 in July. The hour counts are what a duration calculation sees; the day counts are what a calendar calculation sees.

This is why “subtract two timestamps and divide by 86,400” is not the same as “days between two dates”. The first gives elapsed 24-hour periods; the second gives calendar days. For billing periods, rental days, leave balances and age, the calendar answer is usually the one people mean. For timeouts, SLA windows and token lifetimes, the elapsed answer is.

ISO weeks cross the year boundary

ISO 8601 week numbering has its own year, which can differ from the calendar year. Week 1 is the week containing the first Thursday of January, and a week starts on Monday, so the last days of December can belong to week 1 of the next year and the first days of January can belong to week 52 or 53 of the previous one. Measured with the week-number tool:

DateISO weekWeek starts
2026-01-012026-W012025-12-29
2026-12-312026-W532026-12-28
2027-01-012026-W532026-12-28

The last row is the one that breaks naive grouping: a date in January 2027 belongs to ISO year 2026. Any report that groups by calendar year but labels by week number will double-count or lose a week at the boundary. Store the ISO year alongside the week number, not just the week.

Rules that prevent most of these bugs

  • Store instants as UTC (a timestamp or an ISO string with Z), and convert to a local zone only for display.
  • Store calendar dates as dates, with no zone, and decide once which zone gives them meaning.
  • Never parse a bare date with a general-purpose parser without checking what it does; the UTC/local split is implementation-defined.
  • Choose the arithmetic before the answer: elapsed time or calendar days. They differ by an hour twice a year and by more in zones with unusual rules.
  • Keep the ISO year with the ISO week in any weekly report.

Questions this guide raises

Is a Unix timestamp in seconds or milliseconds?

Both conventions exist. Unix itself and most server logs use seconds; JavaScript and many APIs use milliseconds. A 10-digit value is almost certainly seconds and a 13-digit value milliseconds. The tools here let you choose, and the factor is exactly 1,000.

Why is my birthday off by one day in some app?

Most likely the date was stored as an instant (UTC midnight) and displayed in a negative-offset zone, moving it to the previous evening. A birthday is a calendar date, not an instant; it should be stored without a zone and rendered as the same day everywhere.

Does the date-difference tool account for DST?

It counts calendar days, which is what “days between” usually means, and its totals are based on UTC dates. If you need elapsed hours across a DST change, that is a different calculation and depends on the time zone — the tool states the distinction rather than guessing a zone.

What is ISO week 53 and why does it exist?

A year has 53 ISO weeks when it starts on a Thursday, or on a Wednesday in a leap year, because the first Thursday rule puts an extra week in. 2026 is such a year. Reports that assume 52 weeks lose or duplicate a week at the boundary.