Unix, web & database timestamps¶
Unix time¶
POSIX defines seconds since 1970-01-01 00:00:00 UTC, with every day counted as
exactly 86 400 seconds — i.e. leap-second-ignoring
(POSIX.1-2017 §4.16, "Seconds Since the Epoch").
POSIX does not mandate width or signedness; the C time_t was historically signed 32-bit
and is now widely signed 64-bit.
- Worked example:
1577836800= 2020-01-01. - Gotcha: because leap seconds are ignored, a Unix timestamp is not a faithful UTC count across leap boundaries (see time scales).
The Year 2038 problem¶
A signed 32-bit time_t overflows at 2038-01-19 03:14:07 UTC, wrapping to
1901-12-13. The fix is 64-bit time_t. Full detail and the wider rollover family on the
Epoch rollovers page.
Sub-second Unix variants¶
The same 1970 epoch appears in finer units — distinguish them by magnitude:
| Unit | Carrier | Source |
|---|---|---|
| milliseconds | Java System.currentTimeMillis, JavaScript Date |
ECMA-262 §21.4.1.1 |
| microseconds | gettimeofday, PRTime |
gettimeofday(2) |
| nanoseconds | Go time.UnixNano, APFS |
Go time |
ECMA-262 is explicit that JavaScript time is leap-ignoring ("every day … exactly 86,400
seconds") and its Date range is ±8.64 × 10¹⁵ ms. Go's UnixNano is "undefined …
before the year 1678 or after 2262" (int64 ns). The classic misparse is 13-digit ms
read as 10-digit seconds (or vice versa) — off by ~1000×.
WebKit / Chrome (base::Time)¶
Chromium's base::Time counts microseconds since 1601-01-01 UTC (the Windows/FILETIME
epoch), stored as int64. From base/time/time.h:
// microseconds (s/1,000,000) since the Windows epoch (1601-01-01 00:00:00 UTC)
static constexpr int64_t kTimeTToMicrosecondsOffset = INT64_C(11644473600000000);
Used in Chromium History (visits.visit_time), Cookies (creation_utc), and autofill
SQLite stores. Convert: unix_seconds = (value − 11644473600000000) / 1000000.
Not Safari
This is Chrome's "WebKit time" (1601, microseconds). Apple Safari uses CFAbsoluteTime (2001, seconds float) — see the Safari-vs-Chrome trap. They differ by ~395 years and a 10⁶ unit scale.
PostgreSQL¶
timestamp/timestamptz are stored as int64 microseconds since 2000-01-01, per
src/include/datatype/timestamp.h:
#define UNIX_EPOCH_JDATE 2440588 /* date2j(1970, 1, 1) */
#define POSTGRES_EPOCH_JDATE 2451545 /* date2j(2000, 1, 1) */
The two Julian-day anchors differ by 10 957 days = 946 684 800 s — the Unix-seconds value
of 2000-01-01. timestamptz stores the same int64 (UTC internally) and applies the
session zone on I/O; timestamp carries no zone. Very old builds used a float8
(seconds) representation; int64 microseconds is the only representation from PostgreSQL 10
onward.
- Worked example:
0= 2000-01-01;631152000000000= 2020-01-01. - Gotcha: the 2000 epoch is the dominant trap — decoding a PG
timestampagainst the Unix epoch lands 30 years early.
SQLite¶
SQLite has no dedicated date type; the date-and-time functions define three interchangeable encodings:
- TEXT — ISO-8601 strings (e.g.
2025-05-29 14:16:00). - REAL — a Julian day number: "days … since -4713-11-24 12:00:00" (see
calendars). JD
2440587.5= the Unix epoch. -
INTEGER / REAL — a Unix timestamp via the
unixepochmodifier. -
Worked example: JD
2451545.0= 2000-01-01 12:00;1748528160(unixepoch) = 2025-05-29 14:16. - Gotcha: a bare numeric column is ambiguous — the same value can be a Julian day,
Unix seconds, or fractional seconds. SQLite's own
'auto'heuristic even warns that "Unix timestamps for the first 63 days of 1970 will be interpreted as julian day numbers." Julian-day REALs also lose sub-second precision.
MySQL¶
MySQL has two distinct types:
TIMESTAMP— Unix-based, range 1970-01-01 00:00:01 to 2038-01-19 03:14:07 UTC (the Year 2038 ceiling), TZ-converted from the session zone to UTC for storage.DATETIME— a packed calendar literal, range 1000–9999, no TZ conversion.
Both support microsecond fractional seconds (MySQL 5.6.4+).
- Gotcha: a
TIMESTAMPread back in a different session zone displays a different wall-clock time than aDATETIME; reconstructing the original needs the connection'stime_zoneat write time.
Mozilla PRTime¶
Firefox's places.sqlite (moz_places.last_visit_date, moz_historyvisits.visit_date,
…) uses PRTime: signed 64-bit microseconds since 1970-01-01 UTC
(NSPR prtime.h).
- Worked example:
1577836800000000= 2020-01-01. - Gotcha: same epoch as Unix but in microseconds (~16 digits) — the inverse of the WebKit trap (right epoch, wrong unit if read as ms/s). PR_Now is non-monotonic (NTP corrections), so adjacent events can appear out of order.