Unix Timestamps, Time Zones and the Seconds-vs-Milliseconds Bug
Dates and times cause a disproportionate share of bugs. They look simple, and then you meet time zones, daylight saving, leap years and two different definitions of "a timestamp". Here are the ideas that prevent most of the trouble.
What Unix time is
Unix time is the number of seconds that have passed since midnight UTC on 1 January 1970. Right now that number is a little over 1.7 billion. It has two great properties: it's a single integer that's easy to store and compare, and it means the same instant everywhere on Earth. There's no time zone in a Unix timestamp. Time zones only appear when you turn it into something a human reads.
The seconds vs milliseconds bug
Different ecosystems picked different units:
| Seconds | Milliseconds |
|---|---|
Unix/Linux date +%s, PHP time(), Python time.time(), Go Unix(), JWT claims, most databases' epoch functions | JavaScript Date.now(), Java currentTimeMillis(), many JSON APIs, Elasticsearch |
Mix them up and you get one of two symptoms. Treat seconds as milliseconds and every date lands in January 1970, because 1.7 billion milliseconds is only about 20 days. Treat milliseconds as seconds and dates land around the year 57,000.
The quickest check is to count digits. A current timestamp in seconds has 10 digits, in milliseconds 13, in microseconds 16. Our timestamp converter uses exactly that rule to guess the unit. In code, name your variables to make the unit explicit, for example expiresAtMs or createdAtSec. It costs nothing and catches mistakes in code review.
Store UTC, display local
The single most useful rule: store and transmit times in UTC (as Unix timestamps or ISO 8601 strings ending in Z) and convert to local time only at the very edge, when showing it to a person. Servers should run in UTC too. Logs from machines in different regions then line up without mental arithmetic.
ISO 8601 strings are a great interchange format because they're readable and sortable: 2026-09-23T14:05:00Z. If you include an offset, like 2026-09-23T16:05:00+02:00, it still identifies one exact instant.
When UTC isn't enough
There's one important exception. For future events tied to a place, such as a meeting at 9am in New York next March, store the local time plus the IANA time zone name (America/New_York), not a UTC timestamp. Governments change daylight saving rules with surprisingly little notice. If you converted to UTC when the meeting was created and the rules change, your stored instant is now an hour off. Convert to UTC when the event is close, or at display time.
Also store zone names, not offsets. -05:00 is New York in winter but not in summer, and it's also Bogotá, which doesn't observe daylight saving at all.
Daylight saving traps
- Not every day has 24 hours. In zones with daylight saving, one day a year has 23 hours and another has 25. Adding
24 * 60 * 60seconds to "now" doesn't always give "same time tomorrow". Use your date library's calendar arithmetic for that. - Some local times don't exist (the hour skipped in spring) and some happen twice (the hour repeated in autumn). Scheduled jobs set for 2:30am local time can run twice or not at all. Schedule them in UTC.
- "Midnight" can be missing. A few zones have historically switched at midnight, so code that assumes every date starts at 00:00 can break.
JavaScript's Date quirks
JavaScript's Date object stores milliseconds since the epoch, but its parsing rules are inconsistent. new Date("2026-09-23") is treated as UTC midnight, while new Date("2026-09-23T00:00") is treated as local midnight. Months are zero-based in the constructor, so new Date(2026, 8, 23) is September. If you're doing anything beyond basics, the new Temporal API (now arriving in browsers, with polyfills available) or a library like date-fns or Luxon will save you grief.
The year 2038 problem
A signed 32-bit integer can count up to 2,147,483,647 seconds, which runs out at 03:14:07 UTC on 19 January 2038. After that, a 32-bit timestamp wraps around to December 1901. Most modern systems use 64-bit time, but the problem lives on in embedded devices, old file formats and some database column types, MySQL's TIMESTAMP among them. If your application stores long-lived dates such as mortgage terms, certificate expiries or 20-year warranties, it's worth checking now rather than in 2037.
Summary
- Unix time has no time zone. Keep it that way in storage.
- Count digits: 10 for seconds, 13 for milliseconds.
- Store UTC for past events, local time plus zone name for future events.
- Use calendar arithmetic, not "plus 86,400 seconds".
- Schedule recurring jobs in UTC.
Convert epoch seconds or milliseconds to dates and back, with a live clock.
Open the Unix Timestamp Converter