My laptop reports 1717632000 as the Unix timestamp right now. That single integer, the seconds elapsed since midnight UTC on 1 January 1970, is the closest thing software has to a universal clock. Databases, log lines, JWT exp claims, cache TTLs, and cron jobs all rely on it. The Unix timestamp looks trivial; the bugs it generates are not. The two classic ones are the seconds-versus-milliseconds mismatch and the wall-clock-versus-instant confusion, and both have shipped to production at companies you’ve heard of.
What the Unix epoch is and why 1970-01-01 00:00:00 UTC
The Unix epoch is the zero point of POSIX time, defined in IEEE Std 1003.1 as the seconds elapsed since 1970-01-01 00:00:00 Coordinated Universal Time, ignoring leap seconds. POSIX time counts nearly every day as exactly 86,400 seconds; when a leap second is inserted, the count either stalls or repeats, which is why ntpd and chrony have to smear it.
1970 wasn’t chosen for a deep reason. Unix engineers at Bell Labs needed a reference date, the system was running on a 32-bit word machine, and the start of a decade on a 32-bit signed integer gave roughly 68 years of usable range in each direction. That arbitrary choice produced the Year 2038 problem we’ll get to below.
Seconds vs milliseconds — the classic production bug
Most backend APIs, MySQL’s FROM_UNIXTIME, and the date command expect seconds. JavaScript’s Date.now() returns milliseconds. Python’s time.time() returns seconds as a float; time.time_ns() returns nanoseconds as an int. Java’s System.currentTimeMillis() is milliseconds, while Instant.getEpochSecond() is seconds.
Mixing these is one of the highest-frequency date bugs in production. Multiplying a millisecond timestamp by 1000 again pushes the date tens of thousands of years into the future; I’ve seen dashboards render “expires 51791 AD”. Dividing a seconds timestamp by 1000 when the library expects milliseconds renders it as a date in January 1970. Both look like obvious nonsense in isolation, yet neither triggers an exception; the value just silently lands in the wrong decade.
The fix is to know, for every boundary your code touches, which unit lives there. When in doubt, normalise explicitly:
// JavaScript: convert an API-returned seconds timestamp to a Date
const apiSeconds = 1717632000;
const d = new Date(apiSeconds * 1000); // 2024-06-06T00:00:00.000Z
# Python: round-trip from seconds to an ISO 8601 string
import time
secs = 1717632000
iso = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(secs))
# '2024-06-06T00:00:00Z'
If you spot a timestamp that displays as 1970 or somewhere in the fifty-thousands, reach for the Timestamp Converter before reaching for the debugger. Pasting the raw value and seeing what comes back in both seconds and milliseconds is usually enough to identify the mismatch in one step.
UTC, time zones, and why timestamps sidestep them
A wall-clock time is a label a region agrees on. “9:00 in Tokyo” means nothing until you also know the date and that the region is JST. A Unix timestamp is a moment: an absolute point on the world timeline, identical everywhere. 1717632000 is 2024-06-06 09:00:00 JST in Tokyo, 2024-06-06 00:00:00 UTC in London, and 2024-06-05 20:00:00 EDT in New York. Same instant, three labels.
The mistake is storing a wall-clock value as if it were a timestamp. A string like 2024-06-06 09:00:00 without a zone is ambiguous: a database coerces it to the server’s local time, JavaScript parses it as local time, Go parses it as UTC, and the same value round-trips to three different instants. ISO 8601 with an explicit offset (2024-06-06T09:00:00+09:00) fixes this at the human-readable layer; Unix seconds fixes it at the storage layer. Use both: store the integer, render the ISO string.
Converting to and from human-readable dates
Conversion in JavaScript is straightforward, but the Date object has traps. The constructor and the getDate()/getHours() family use local time, while getUTCDate(), getUTCHours(), and toISOString() use UTC. Mixing them is a common source of off-by-one-day bugs in CI runs that execute in UTC against code developed in a non-UTC zone.
// Safe conversion in either direction, always in UTC
const ts = Math.floor(Date.now() / 1000); // ms → seconds
const isoUtc = new Date(ts * 1000).toISOString(); // '2024-...Z'
const backToSeconds = Math.floor(Date.parse(isoUtc) / 1000);
In Python, prefer datetime with explicit timezone objects rather than naive datetimes. datetime.fromtimestamp(ts, tz=timezone.utc) is unambiguous; datetime.fromtimestamp(ts) silently uses local time and produces different values when run on different machines.
When the conversion involves arithmetic, like adding days or computing the difference between two instants, the Date Calculator handles the calendar rules for you so you don’t accidentally assume every month has 30 days.
The 2038 problem
A signed 32-bit integer maxes out at 2147483647, which corresponds to 2038-01-19 03:14:07 UTC. One second later, on systems still storing time as time_t in a 32-bit signed int, the value wraps to -2147483648 and reads as December 1901. This is Y2038, the spiritual cousin of Y2K but with a harder edge: the overflow is real on 32-bit Linux kernels, on legacy embedded firmware, and inside on-disk binary formats that baked a 32-bit time_t into their structs.
The mitigation on modern systems is already shipped. Linux on 64-bit architectures uses a 64-bit time_t with a range of roughly 292 billion years, and glibc on 32-bit architectures now supports a 64-bit variant. The remaining exposure is in long-lived embedded devices, file system metadata formats, and binary protocols that defined their timestamp field width decades ago. If you’re designing a new format today, make timestamp fields 64-bit signed integers and count seconds.
FAQ
Does a Unix timestamp include the time zone?
No. A Unix timestamp represents a single instant on the world timeline and means the same thing in every time zone. The zone only appears when you render the timestamp into a human-readable date. 1717632000 is always the same moment; whether it prints as 2024-06-06 09:00 in Tokyo or 2024-06-05 20:00 in New York depends entirely on the zone you format it in.
Why does my timestamp show as a date in 1970?
You passed seconds to a function that expected milliseconds, or vice versa. JavaScript’s Date constructor takes milliseconds, so feeding it a seconds value like 1717632000 produces a date in January 1970. Multiply by 1000 before constructing the Date. The mirror-image bug, feeding milliseconds to a seconds-expecting API, produces a date roughly 50,000 years in the future.
Are leap seconds included in Unix time?
Officially no. POSIX defines a day as exactly 86,400 seconds and pretends leap seconds don’t exist, so Unix time stalls or repeats during an inserted leap second. In practice, daemons like chrony and Google’s ntpd smear the leap second across a window so applications see a smooth clock. If you need sub-second accuracy across a leap-second boundary, use a monotonic clock for elapsed measurement and smear the wall clock for absolute time.
Pick a unit, label it, move on
The next time a timestamp lands in your lap, do three things before writing conversion code: identify whether the value is seconds or milliseconds, confirm the zone the source assumed, and verify against a known instant. Paste the raw integer into the Timestamp Converter to sanity-check before you write the new Date(...) call. Two minutes at the boundary saves a four-hour incident at 2 a.m.