A timestamp is not a clock. It is an address.

Read the address with one equation. Measure intervals with a second clock, the monotonic ruler. Learn to tell them apart and every famous time bug turns readable.

Unix time epoch explained, from one equation to two clocks

A timestamp is not a clock. It is an address on the UTC number line, fixed by one equation. A second clock, the monotonic ruler, measures intervals. Learn both and every famous time bug turns readable.

Unix time epoch explained: it is an address, not a clock#

Most explainers open with a definition and then link out to a converter. That teaches you to look up a value, never to reason about one. Instead, start with the reframe that makes everything else obvious. A timestamp does not tell the time. It names a position.

Think of one long number line measured in seconds. Zero sits at the epoch, midnight UTC on 1 January 1970. Every instant since then is a distance along that line. Therefore a timestamp is an address, and reading a clock face is just projecting that address into a local timezone. Because the address is a plain count of seconds, it carries no timezone at all. That single idea, unix time epoch explained as a position rather than a reading, is what the rest of this article keeps returning to.

The one equation everything hangs on#

The whole system rests on a single invariant that ranking pages rarely state. A Unix timestamp is whole days since the epoch, multiplied by the seconds in a day, plus the second within the current day.

One integer, three wall clocks#

Here is the address idea made concrete. Take one integer and render it on three different clock faces. The faces disagree on the label, yet they name the same instant. Change the number below and watch all three move together.

One integer, three wall clocks

UTC

UTC +00:00

2023-11-14

22:13:20

Kyiv

UTC +02:00

2023-11-15

00:13:20

Kiribati (Kiritimati)

UTC +14:00

2023-11-15

12:13:20

Same integer, same instant. The three clocks disagree on the label, never on the address. That is why a timestamp needs no timezone to be exact.

A timestamp is one point on the UTC line. A timezone is a projection of that point onto a local clock face. Change the integer and all three faces move together. With JavaScript off, the faces for 1700000000 are shown.

Notice what did not happen. The integer never needed a timezone to be exact. Moreover the three faces are not three times. They are one address, drawn in three local dialects. That is why storing a UTC timestamp and rendering it locally is the correct default for a past instant.

Compute it by hand, both directions#

A converter hands you an answer and hides the work. So let us do the opposite and derive the value in both directions. This is unix time epoch explained the way a lookup tool never will, by hand. The panel below drives the arithmetic; flip the view to switch which way you are computing.

Derive an epoch address by hand, both directions

Worked example: turn 1700000000 into a UTC date.

  1. days = floor(1700000000 / 86400)19675
  2. second_of_day = 1700000000 - 19675 x 8640080000
  3. clock = 80000 s split into h : m : s22:13:20
  4. date = day 19675 counted from 1970-01-012023-11-14

Result22:13:20 UTC on 14 Nov 2023

Flip the view to choose a direction, enter a value, and derive it step by step. The invariant unix = 86400 x days_since_epoch + second_of_day does all the work. With JavaScript off, the worked example for 1700000000 is shown below.

Timestamp to date: 1700000000 step by step#

Take the real value 1700000000. First divide by the seconds in a day. That gives 1700000000 / 86400 = 19675 whole days with a remainder of 80000 seconds. Next split the remainder into a clock. Because 80000 seconds is 22 hours, 13 minutes, and 20 seconds, the time of day is 22:13:20. Then count 19675 days forward from 1970-01-01, which lands on 2023-11-14. Therefore the address resolves to 22:13:20 UTC on 14 November 2023. The same computation runs in every language.

js
// Timestamp -> UTC date, straight from the invariant.
const ts = 1700000000;
const days = Math.floor(ts / 86400);   // 19675
const sod  = ts - days * 86400;        // 80000  (second_of_day)
const date = new Date(ts * 1000);      // 22:13:20 UTC, 14 Nov 2023
console.log(days, sod, date.toISOString());

Date to timestamp, with sanity anchors#

The reverse direction runs the invariant forward. First count whole days from the epoch to your date. Then multiply by 86400 and add the seconds into the day. Two anchors let you check any value you derive. The epoch itself is unix second 0, which is midnight UTC on 1 January 1970, and midnight UTC on 18 July 2026 is unix second 1784332800. If your arithmetic reproduces both, trust it.

to_unix.py · python
import datetime as dt

def to_unix(y, m, d, hh, mm, ss):
    # whole days since the epoch, then apply the invariant
    days = (dt.date(y, m, d) - dt.date(1970, 1, 1)).days
    return days * 86_400 + hh * 3600 + mm * 60 + ss

# Two anchors let you self-check any value you derive.
assert to_unix(1970, 1, 1, 0, 0, 0)     == 0
assert to_unix(2023, 11, 14, 22, 13, 20) == 1_700_000_000
assert to_unix(2026, 7, 18, 0, 0, 0)     == 1_784_332_800

Why Unix time ignores leap seconds#

People ask why POSIX time ignores leap seconds, and the honest answer is not folklore. It falls straight out of the invariant. The definition of a Unix day forces it.

The 86,400-second day is a definition, not a measurement#

Astronomers occasionally add a leap second to UTC to keep clocks aligned with the Earth's slightly irregular spin. Unix time cannot represent that extra second. Because a Unix day is defined as exactly 86400 seconds, the counter has no slot for a 61st second. So it is not a faithful tally of physical seconds since 1970. It is a tally of defined days, and defined days never vary in length.

The leap second, second by second#

Watch the seam where a leap second is inserted. The clock is supposed to read three values in order. Unix time, however, has an integer for only two of them.

  1. 23:59:59

    The last ordinary second

    UTC and Unix time agree. The counter is about to tick over to the next day.

  2. 23:59:60

    The inserted leap second

    UTC adds a 61st second. Unix time has no integer for it, so this moment cannot be represented at all.

  3. 00:00:00

    The repeated or stepped second

    Unix time reuses the previous value or steps back. A wall-clock duration measured across the seam can come out negative.

STEP versus SMEAR: the failure mechanism#

The insertion is not just trivia. It is a real outage mechanism, and there are two ways to handle it. A STEP inserts or repeats a second, so a monotonic-looking wall-clock read can move backward. A SMEAR instead stretches each second near midnight, so the clock keeps moving forward and never repeats. The STEP path is what took down services in 2012 and 2015.

Two ways to absorb a leap second, and how each one fails
BehaviorSTEP: insert 23:59:60SMEAR: stretch the second
What happensA 61st second is inserted; 23:59:59 repeatsEach second near midnight is made slightly longer
Clock still monotonic?No: the same second can read twiceYes: the clock only moves forward
Failure modenow - then < 0; duplicate timestamp-keyed rowsNo jump, but the clock is briefly off UTC
Who runs itDefault NTP and older kernels (2012, 2015)Google, AWS, and Cloudflare public NTP

The code makes the STEP failure concrete. When the same second reads twice, a naive duration goes negative and a guard clause fires.

leap_step.py · python
# A leap second inserted as a STEP repeats 23:59:59, so a wall-clock read
# can move backward across the seam.
start = time.time()       # ...T23:59:59.6
# leap second: the wall clock repeats the second
end   = time.time()       # ...T23:59:59.1  (earlier than start)
elapsed = end - start     # negative: a duration that should never exist
if elapsed < 0:
    raise RuntimeError("time went backward")  # the 2012 and 2015 outage class

Why it overflows: the Y2038 derivation#

The same invariant that hides leap seconds also predicts the Y2038 problem. The 32-bit overflow is not a mystery date to memorize. You can derive the exact boundary from the seconds in a day.

2,147,483,647 divided by 86400: land the boundary#

A signed 32-bit time_t can hold values up to INT32_MAX = 2147483647. Walk the division one step at a time.

  1. Start from the largest signed 32-bit value

    A signed 32-bit time_t maxes out at INT32_MAX = 2147483647 seconds.

  2. Divide by the seconds in a day

    2147483647 / 86400 = 24855 whole days, with a remainder of 11647 seconds.

  3. Turn the remainder into a clock

    11647 seconds is 03:14:07, that is three hours, fourteen minutes, and seven seconds.

  4. Count the days from the epoch

    Day 24855 counted from 1970-01-01 lands on 2038-01-19.

  5. Read the boundary

    The last representable second is 03:14:07 UTC on 19 January 2038. One tick later, the counter overflows.

The +1 tick that wraps 136 years back#

Now advance one second past that boundary. A signed counter cannot hold 2147483648, so it wraps to INT32_MIN = -2147483648. Feed that negative value back through the invariant and it resolves to 20:45:52 UTC on 13 December 1901. In one tick the clock jumps 136 years into the past. Snap to the boundary below and advance it yourself.

The Y2038 boundary, one tick at a time
  1. last safe second = INT32_MAX = 214748364703:14:07 UTC on 19 Jan 2038
  2. +1 overflows to INT32_MIN = -214748364820:45:52 UTC on 13 Dec 1901

One tick past the boundary, a signed counter jumps from 2038 back to 1901. That single wrap is the whole Y2038 problem.

Snap to the largest value a signed 32-bit time_t holds, then advance one second and watch the clock wrap 136 years into the past. With JavaScript off, the boundary and its wrap are listed below.

Signed versus unsigned: 2038 versus 2106#

The sign bit is the whole story. Read the same 32 bits as unsigned and the ceiling moves. An unsigned counter has no negative half, so it keeps climbing for another 68 years before it too runs out.

How long each 32-bit counter survives68 years apart

2038-01-19

Signed 32-bit time_t

68 more years

2106-02-07

Unsigned 32-bit counter

A signed counter dies at 03:14:07 UTC on 19 January 2038. Reading the same bits as unsigned survives to 06:28:15 UTC on 7 February 2106 (UINT32_MAX = 4294967295), because the sign bit becomes one more data bit.

Show data table
How long each 32-bit counter survives (last representable instant (UTC))
Optionlast representable instant (UTC)
Signed 32-bit time_t2038-01-19
Unsigned 32-bit counter2106-02-07

Widening the type does not fix data already written#

The other clock: the monotonic ruler#

The address is only half the model. A second clock answers a different question. When you measure how long something took, you do not want an address at all. You want a ruler.

A ruler you measure intervals with, never an address#

A monotonic clock has a meaningless origin. It might count from the last boot or from some arbitrary point. Only the difference between two reads carries meaning. Crucially, it never jumps backward. The wall clock, by contrast, is an address that can be reset, stepped by NTP, folded by DST, or restored from a VM snapshot. So the two clocks answer two questions, and the diagram below keeps them apart.

The wall-clock address versus the monotonic rulerThe wall clock names an instant and can jump. The monotonic clock measures an interval and only ever counts up. Ask for an address, or ask for a duration, but never confuse the two.

Watch a wall clock go backward#

This is the bug the SERP mentions in one line and never shows. Measure an interval with a wall clock, then let the clock get corrected mid-measurement. The naive elapsed, end - start, can go negative. Start a measurement below, inject a fault, then stop and compare the two clocks.

Two clocks lab: measure an interval while time misbehaves

A request starts, and four seconds of real work later it ends. During those four seconds NTP steps the wall clock back seven seconds:

  • Wall clock says: end - start = -3.0 s (a negative duration).
  • Monotonic ruler says: end - start = +4.2 s (the honest interval).

The wall clock is an address and an address can move. Measure an interval with it and a backward step makes the duration negative. The monotonic ruler has no address to move, so it cannot lie about the interval.

Start a measurement, inject a clock fault, then stop. The naive elapsed = end - start read from the wall clock breaks; the monotonic ruler does not. With JavaScript off, a worked example is shown below.

The monotonic ruler stayed honest through every fault. Therefore the rule is simple. Timestamp events with the wall clock, and measure durations with the monotonic clock. Mixing them is how a backoff underflows and a timeout check inverts.

Monotonic in Go, Rust, and Linux#

Every runtime exposes the ruler, though the names differ. In Go the monotonic reading rides along inside time.Now(). In Rust it is Instant::now(). On Linux it is clock_gettime(CLOCK_MONOTONIC). Here is the same measurement in all three.

go
// time.Now() carries a monotonic reading; subtraction uses it automatically.
start := time.Now()
doWork()
elapsed := time.Since(start)   // immune to wall-clock jumps

Address or ruler? Classify every task#

The model becomes a tool the moment you classify a job before writing code. That is unix time epoch explained as a decision procedure, not a lookup. So ask one question first. Are you naming an instant, or measuring an interval? The answer picks the clock, the storage type, and the timezone handling.

The decision flow#

Walk the branches in order. Measuring an interval sends you to the ruler. Naming a past instant sends you to the wall-clock address. A future local event needs a third path entirely, which the next section explains.

Do I need the address or the ruler?Start from the job. An interval wants the monotonic ruler. A past instant wants the UTC address. A future local event wants local wall-time plus an IANA zone. A pure calendar value wants a date with no zone at all.

Task to clock, storage type, precision, timezone#

The same branches fit into a lookup table keyed by the actual job. Keep it close when you design a schema.

Which clock, storage type, precision, and timezone each job needs
TaskClockStore asPrecisionTimezone
Timestamp an eventWall (address)UTC seconds or RFC 3339Seconds or msUTC, render local
Measure latency or backoffMonotonic (ruler)Nothing; keep a deltaNanosecondsNone
Schedule a future deadlineWall (address)Local wall-time + IANA zoneMinutesIANA zone id
Order events across nodesWall + logicalUTC plus a logical counterMs + sequenceUTC
Store a birthdayCalendarA date, no time of dayDayNone
Store a recurring meetingWall (address)Local time + rule + zoneMinutesIANA zone id

The future-timestamp storage trap#

Here is the case every ranking page gets wrong. The advice to store UTC and display local is correct only for a past instant. A future local event is different. Because a government can move a DST boundary before the date arrives, freezing UTC now bakes in a rule that might change. So a deadline or a recurring meeting must be stored as local wall-time plus an IANA zone, then resolved to UTC at read time.

future_event.sql · sql
-- WRONG for a future local event: freezing UTC now bakes in today's rule.
-- If the government later moves the DST boundary, the meeting shifts an hour.
INSERT INTO meetings (starts_at_utc) VALUES ('2027-03-15 08:00:00');

-- RIGHT: store the local wall-time plus the zone, resolve to UTC at read time.
INSERT INTO meetings (local_time, iana_zone)
VALUES ('2027-03-15 09:00:00', 'Europe/Kyiv');

Every famous bug, decoded by the model#

Now the model pays off. Each famous class of time bug reduces to one of two faults. Either the address broke an invariant, or someone used the address as a ruler. Read the catalogue and the postmortems stop being war stories and start being predictions.

Every famous class of time bug, decoded by the address-versus-ruler model
Bug classWhat brokeWhich rule it violates
Seconds vs millisecondsA timestamp read 1000x too large or too smallWrong unit on the address
Y2038 overflowA signed 32-bit counter wrapped to 1901The address outgrew its integer
Leap-second outage (2012, 2015)now - then < 0 crashed running daemonsMeasured an interval with the address
DST foldOne local time named two real instantsAmbiguous address, no zone stored
Negative durationA backoff underflowed after a clock setUsed the address as a ruler
Future-deadline breakageA meeting moved when a DST rule changedFroze UTC for a future local event

When NOT to reach for Unix time#

The address is powerful, yet it is the wrong tool for several common jobs. A Unix timestamp is a point on the UTC line, so anything that is not such a point does not belong in one.

Reference: storage types and the epoch zoo#

Two tables are worth returning to. The first maps database column types to the ranges they cover. The second lists the day-zero of five different systems, because Unix is not the only epoch you will meet.

Storage types for an instant, and the ranges they cover
TypeRangeZone-aware?Use it for
MySQL TIMESTAMP1970 to 2038 (signed 32-bit)Converts to the session zoneInstants inside the range
MySQL DATETIME1000 to 9999No zone storedWall-time you store literally
Postgres TIMESTAMPTZMicrosecond, very wide rangeStores UTC, renders localAlmost every instant
Postgres TIMESTAMPMicrosecond, very wide rangeNo zone storedLocal wall-time, resolve later
Unix seconds (int64)About 292 billion yearsNone; always UTCPortable event timestamps
The epoch zoo: five systems, five different day zero
SystemEpoch (day zero)Tick sizeNotes
Unix / POSIX1970-01-01 UTC1 secondThe epoch this article derives
Windows FILETIME1601-01-01 UTC100 nanosecondsStart of the Gregorian cycle
NTP1900-01-01 UTCabout 232 picosecondsIts own 32-bit era rolls over in 2036
Apple / Cocoa2001-01-01 UTC1 secondThe NSDate reference date
GPS1980-01-06 UTC1 secondNo leap seconds, so now ahead of UTC

Sources#

Every value here was computed against primary specifications, not secondary blogs. The seconds-since-the-epoch formula and the 86400-second day come from the POSIX base definitions, chapter 4 (opens in new tab). Clock resolution and the monotonic clock identifiers are defined in the POSIX clock_getres and clock_gettime page (opens in new tab). The authoritative record of every inserted leap second is the IANA leap-seconds.list (opens in new tab). In 2022 the CGPM voted to stop inserting leap seconds by 2035, recorded in BIPM CGPM 2022, resolution 4 (opens in new tab). The timestamp string format used throughout is RFC 3339 (opens in new tab).

This piece sits beside our other deep dives into what the machine actually does. If you liked deriving the numbers here, our walkthrough of how computers generate randomness takes the same first-principles approach to entropy. Because the monotonic clock is really a hardware timer, our explainer on why CPU cache outperforms RAM is the companion read on where those cycles come from. And since every HTTP response carries a wall-clock date and a TLS validity window, what happens when you hit a URL shows these timestamps in flight. For the full set, browse more software engineering deep-dives on the Atyantik technical blog.

Time bugs are quiet until they are a 3 a.m. incident, and they usually surface as a negative duration or a certificate that reads as not-yet-valid. If you want a second set of hands on a timezone migration, a Y2038 audit of serialized data, or an event schema that will not fold under DST, we are glad to help. No pressure and no lock-in.

Talk to our software engineering team

Keep reading