Timestamp 0: The Beginning of Unix Time
Unix timestamp 0 is January 1, 1970 at 00:00:00 UTC. Every Unix timestamp is the number of seconds elapsed since this moment. Timestamp 1 is one second later. Timestamp 86400 is exactly one day later, January 2, 1970.
# Verify in a terminal
date -d @0 # Linux
date -r 0 # macOS
# Both output: Thu Jan 1 00:00:00 UTC 1970
The epoch itself has no special behavior. It is just zero. But understanding it explains every timestamp you encounter in logs, databases, APIs, and debugging sessions.
How the Epoch Was Chosen
Unix was developed at Bell Labs starting in 1969. The original PDP-7 implementation from 1969 used a different time base; the timestamp format stabilized around 1971 to 1973 as Unix was ported to the PDP-11.
The choice of January 1, 1970 was pragmatic rather than principled:
- It was close to the current date during development, maximizing the useful range of positive timestamps.
- On a 32-bit system, a signed integer gives roughly 68 years of positive values (2^31 seconds ≈ 68.05 years). Starting from 1970, that range extends to 2038. That was more than enough for the foreseeable future in 1971.
- A calendar date boundary (January 1) at midnight UTC was a natural, unambiguous starting point with no off by one ambiguity.
Other early systems made different choices. MULTICS (a predecessor to Unix) used a different epoch. Windows NT chose January 1, 1601 because it’s the start of a 400-year Gregorian calendar cycle. There is no universal standard. Unix just became dominant enough that its epoch did too.
Negative Timestamps: Before 1970
A timestamp of -86400 is December 31, 1969 at 00:00:00 UTC. Going further back, -2208988800 is January 1, 1900, the epoch used by NTP (Network Time Protocol).
Most modern programming environments handle negative timestamps correctly:
import datetime
# December 31, 1969
datetime.datetime.fromtimestamp(-86400, tz=datetime.timezone.utc)
# datetime.datetime(1969, 12, 31, 0, 0, tzinfo=datetime.timezone.utc)
# Year 1000
datetime.datetime(1000, 1, 1, tzinfo=datetime.timezone.utc).timestamp()
# -30610224000.0
// JavaScript
new Date(-86400 * 1000).toISOString()
// "1969-12-31T00:00:00.000Z"
Watch out for APIs and database columns that use unsigned integers. They cannot represent negative values, so pre-1970 dates are either rejected or silently stored incorrectly.
Seconds vs Milliseconds: A Persistent Source of Bugs
Unix time is defined in seconds. The C standard library, POSIX date, and shell commands all work in seconds. But JavaScript’s Date object (and by extension, most browser APIs and a large fraction of web services) uses milliseconds:
Date.now() // milliseconds since epoch, e.g. 1700000000000
Math.floor(Date.now() / 1000) // convert to seconds: 1700000000
The canonical bug: you read a timestamp in seconds from a database, pass it directly to new Date() in JavaScript without multiplying by 1000, and get a date in January 1970 instead of 2023. The inverse (multiplying a millisecond timestamp by 1000) gives a date in the distant future.
Some APIs are inconsistent about which they use. When consuming or producing timestamps, document the unit explicitly. A comment or field name like created_at_ms vs created_at_seconds prevents the mismatch at the call site.
How Different Systems Represent the Same Moment
The same moment, January 1, 2024 at 00:00:00 UTC, looks very different depending on the system:
| System | Representation | Notes |
|---|---|---|
| Unix (seconds) | 1704067200 | Standard C/POSIX |
| Unix (milliseconds) | 1704067200000 | JavaScript, Java |
| Unix (microseconds) | 1704067200000000 | PostgreSQL timestamp |
| Windows FILETIME | 133491648000000000 | 100-ns since 1601-01-01 |
| Excel serial | 45292 | Days since 1900-01-01 |
| GPS time | 1388275218 | Seconds since 1980-01-06, no leap seconds |
When converting between these, two things matter: the epoch offset and the unit. For Windows FILETIME, the offset is 11644473600 seconds (the difference between 1601-01-01 and 1970-01-01), and the unit conversion is dividing by 10^7.
Leap Seconds and Why Unix Ignores Them
UTC periodically inserts leap seconds to keep atomic clock time aligned with Earth’s rotation. Since 1972, 27 leap seconds have been added. Unix time ignores this: it defines every day as exactly 86400 seconds, so UTC days with a leap second are 86401 seconds long, but Unix time still advances by 86400.
The practical effect is that Unix timestamps are not monotonically consistent with UTC for dates that contain leap seconds. The difference accumulated since 1970 is 27 seconds. For most applications, this is irrelevant. For systems requiring subsecond accuracy against physical time (telescope pointing, scientific measurements, some financial systems), it matters, and those systems either use TAI (International Atomic Time, which counts every second including leap seconds) or maintain a lookup table of leap second offsets.
Reading Timestamps in Practice
When you see a suspicious number in a log file or database, these ranges help identify what unit it’s in:
~10 digits: Unix seconds (e.g., 1700000000 → Nov 2023)
~13 digits: Unix milliseconds (e.g., 1700000000000 → Nov 2023)
~16 digits: Unix microseconds (e.g., 1700000000000000 → Nov 2023)
~18 digits: Windows FILETIME (e.g., 133000000000000000)
A quick check in Python:
import datetime
ts = 1700000000
datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
# datetime.datetime(2023, 11, 14, 22, 13, 20, tzinfo=datetime.timezone.utc)
If the result is obviously wrong (year 1970, year 2554), divide or multiply by 1000 and try again.