Negative Unix Timestamps and Dates Before 1970
· bugsfundamentals
-86400 is 31 December 1969. Most languages handle it correctly; a surprising number of databases, APIs and validators do not.
The Unix epoch is a reference point, not a floor. Negative values count seconds backwards from 1 January 1970:
| Timestamp | Instant |
|---|---|
-1 |
31 Dec 1969, 23:59:59 UTC |
-86400 |
31 Dec 1969, 00:00:00 UTC |
-2208988800 |
1 Jan 1900, 00:00:00 UTC |
-2147483648 |
13 Dec 1901, 20:45:52 UTC — the signed 32-bit floor |
This is well defined and unambiguous. The problem is that a great deal of software assumes timestamps are non-negative.
Where it breaks
Unsigned columns and types. A BIGINT UNSIGNED column, a uint64 field, or a protobuf uint32 cannot store a negative timestamp. Depending on the system you get a rejection, a wrap to an enormous positive number, or a silent clamp to zero — which reads as 1 January 1970.
MySQL TIMESTAMP. Its range starts at 1970-01-01 00:00:01 UTC. Anything earlier is rejected or zeroed. DATETIME goes back to year 1000 and should be used instead. See storing timestamps in databases.
Validation that assumes positive. if (!timestamp) treats 0 as absent, and range checks written as timestamp > 0 reject every pre-1970 date. Both are extremely common.
Excel and spreadsheets. Excel cannot represent dates before 1 January 1900 at all, and its serial numbering makes pre-1900 arithmetic meaningless. A CSV round-trip through Excel can destroy historical dates.
Older JavaScript engines and libraries. Modern Date handles negatives correctly, but plenty of formatting libraries, date pickers and validators do not, particularly around the 1900 boundary.
The 2106 “fix”. Switching to an unsigned 32-bit timestamp postpones overflow to 2106 but makes all pre-1970 dates unrepresentable. It is a workaround for constrained embedded systems, not a general solution.
Why this shows up as a birth-date bug
Birth dates are the most common pre-1970 data in ordinary business systems. Anyone born before 1970 has a negative birth timestamp, so a system that mishandles negatives works perfectly in testing — where everyone is younger — and fails for a large fraction of real users.
The characteristic symptom is a birth date silently becoming 1 January 1970, which then produces an age of fifty-something for everyone affected.
Language behaviour
Most modern languages are correct:
from datetime import datetime, timezone
datetime.fromtimestamp(-2208988800, tz=timezone.utc) # 1900-01-01 00:00:00+00:00
new Date(-2208988800 * 1000).toISOString(); // '1900-01-01T00:00:00.000Z'
time.Unix(-2208988800, 0).UTC() // 1900-01-01 00:00:00 +0000 UTC
The failures are almost never in the language core — they are in a column type, a serialisation format, a validator, or an intermediate service.
Historical dates get genuinely strange
Below about 1900, timestamps stop corresponding neatly to what people wrote at the time:
Local Mean Time. Standardised time zones are a nineteenth-century invention. Before then, each town kept its own solar time. The IANA database records this: converting an 1880 timestamp to Europe/London gives an offset of -00:01:15, London’s actual mean solar offset. Correct, and surprising.
The Gregorian changeover. Britain and its colonies switched from the Julian calendar in September 1752, skipping eleven days — 2 September was followed by 14 September. Russia switched in 1918, Greece in 1923. Unix time uses the proleptic Gregorian calendar, projecting Gregorian rules backwards indefinitely, so a converted date before the changeover will not match the date written in contemporary documents.
For genealogy, historical research or anything touching pre-1900 records, store the original calendar date as text alongside any computed timestamp. The timestamp alone loses the distinction.
Practical guidance
- Use signed 64-bit integers for timestamps everywhere.
- Avoid MySQL
TIMESTAMP; useDATETIME. - Never write
if (!ts)orts > 0as a presence check — use an explicit null check. - Include a pre-1970 date in your test fixtures.
-2208988800(1 Jan 1900) is a good one. - For dates before about 1900, store the original text as well as the timestamp.
- Test that your date picker, API layer and database all accept a birth date in the 1950s.
The converter handles negative values — try -2208988800 to see the 1900 boundary, or -1 for the second before the epoch.