DEV Community

Cover image for A Practical Guide to Time for Developers: Part 2 — How one computer keeps time (Linux)
Dmytro Huz
Dmytro Huz

Posted on • Originally published at dmytrohuz.com

A Practical Guide to Time for Developers: Part 2 — How one computer keeps time (Linux)

Time on a computer looks simple: call now(), get a timestamp, move on.

In the previous article, we discussed the idea that time is just a single point on a timeline. The crucial part is defining which timeline that point belongs to.

For computers, this matters a lot. A system effectively works with two timelines: real time and boot time. You can convert between them, but they are different measuring systems and shouldn’t be used interchangeably—because each timeline serves a different purpose.

  • Real time answers: “What time is it in the real world right now?”
  • Boot time answers: “How much time has passed since X (boot)?”

timeline

The computer has has an ecosystem - a few components: hardware and software to track and calculate different times in both timelines.

This part explains that ecosystem using one diagram, top to bottom. The diagram is the spine; everything else is commentary that makes it click: where ticks come from, what the hardware pieces do, why there are multiple “system times”, what suspend breaks, where interrupts fit, and how epoch nanoseconds become “Tuesday 14:03 in Vienna”.

Figure 1 — The whole pipeline

Pipeline
Four lanes:

  • RTC: survives power-off, keeps “wall time”
  • CPU counter (often TSC): ticks while the CPU runs
  • Kernel timekeeper: turns ticks into several clocks
  • Userspace: calls clock_gettime() and formats time for humans

Now: top → bottom.


1) Boot & Initialization Phase

1.1 RTC: the “battery clock”

At the top-left sits the RTC. Think of it as the tiny clock that keeps time while the computer is asleep or powered off. It usually stores calendar-ish values (year/month/day/hour/min/sec). It’s not “nanoseconds since 1970” by nature — that’s something software creates later.

RTC exists so the system doesn’t boot into the void. Without it, everything starts at “some default” until NTP/PTP (or a human) sets the time.

1.2 Turning RTC into “Unix time”

Next step: the kernel reads RTC and converts it into Unix epoch time (seconds + nanoseconds since 1970-01-01T00:00:00Z). That gives a sensible starting point for time-of-day.

This is still a bootstrap value. RTC isn’t a precision clock. It’s a “good enough to start” clock.

1.3 The CPU counter: ticks while running

Now the machine is awake, so Linux wants something faster and more stable than “ask the RTC all the time.” Enter the CPU/platform counter — the clocksource. On modern x86, that’s often the TSC.

Important mindset shift:

TSC is not “the time.”

TSC is “how many ticks happened since some arbitrary start.”

It’s a counter. It’s only meaningful after conversion.

1.4 Calibration: making ticks speak nanoseconds

Ticks are just ticks until the kernel knows the counter’s rate. That’s why the diagram shows calibration: “cycles per second”.

Linux maintains conversion parameters so it can cheaply do:

  • delta ticks → delta nanoseconds

That’s the key: Linux mostly cares about deltas.

1.5 Boot finishes by aligning the timelines

At the end of boot, two things are true:

  • there’s an “elapsed since boot” timeline (monotonic) starting at 0
  • there’s a “wall clock” timeline (realtime) aligned to the RTC-derived epoch time

The clean relationship is:

CLOCK_REALTIME = CLOCK_MONOTONIC + wall_clock_offset

At boot, the kernel chooses the offset so realtime matches RTC.

This one relationship explains half of the weirdness people hit later.


2) Runtime Phase (Continuous Tracking)

2.1 Where ticks come from (without going into physics)

Under the hood, some oscillator ticks, hardware counts those ticks, and Linux reads the count. That’s it at the conceptual level.

The only thing worth memorizing here:

The hardware gives ticks. The OS gives meaning.

2.2 The kernel’s “working memory”

In the middle of the diagram there’s a box of variables. That box is basically the kernel’s timekeeping brain:

  • tsc_now, tsc_last — current and previous counter snapshot
  • mult, shift — ticks→ns conversion
  • mono — accumulated elapsed time since boot
  • suspend_ns — time spent asleep (so BOOTTIME can include it)
  • wall_off — the offset that turns monotonic into realtime

With those, Linux can build the clock APIs that user space expects.


3) Time requests: clock_gettime() makes everything happen

This part of the diagram is the “action scene”:

  • userspace calls clock_gettime(CLOCK_...)
  • kernel reads the counter (RDTSC in the TSC world)
  • kernel updates its state (delta ticks → delta ns → accumulate)
  • kernel returns the requested clock

A useful mental shortcut:

The kernel doesn’t need a metronome to keep time moving.

It can compute “now” on demand by reading a running counter.

(Internally there are periodic activities too, but this is the clean model.)

The tiny update loop

The heartbeat is:

  • delta_ticks = tsc_now - tsc_last
  • delta_ns = ticks_to_ns(delta_ticks)
  • mono += delta_ns

Everything else is derived from mono.


4) Kernel clocks: three timelines, three different promises

Now the diagram derives the clocks.

4.1 CLOCK_MONOTONIC — for durations

The diagram says:

CLOCK_MONOTONIC = mono

That’s the “elapsed time” clock.

It’s the clock to use for anything that needs to be sane even if wall time changes:

  • timeouts
  • retries
  • rate limiting
  • latency measurements
  • “sleep for X”

It doesn’t go backwards, and it doesn’t jump when someone sets the wall clock.

4.2 CLOCK_REALTIME — for human time

The diagram says:

CLOCK_REALTIME = mono + wall_off

(setting time changes wall_off, not mono)

This is the sentence.

Realtime is epoch-based wall time. It’s the one that becomes “2026-03-05 13:00:00”.

Because it must match the outside world, it’s adjustable (NTP/PTP/manual set), and that means it can jump. It’s great for timestamps, terrible for measuring elapsed time.

4.3 CLOCK_BOOTTIME — monotonic that includes sleep

The diagram says:

CLOCK_BOOTTIME = mono + suspend_ns

Suspend is where people get surprised: monotonic often pauses while the system sleeps. BOOTTIME exists for the “time since boot including sleep” definition.


5) Power management: suspend/resume is where the split matters

During suspend:

  • CPU isn’t running
  • counters may stop or aren’t sampled
  • mono doesn’t move (in the simple model)
  • real world keeps moving

So the diagram does a clever but simple thing:

  • store persistent time at suspend (often RTC)
  • store persistent time at resume
  • difference = sleep delta
  • add it to suspend_ns so BOOTTIME advances across sleep
  • keep wall clock aligned after resume (effectively by updating the offset)

That’s why BOOTTIME exists and why wall time remains useful after sleep.


6) From epoch nanoseconds to “Tuesday 14:03 in Vienna”

Kernel time is a number. Humans want a calendar.

On Linux, CLOCK_REALTIME is typically represented as nanoseconds since the Unix epoch (1970-01-01T00:00:00Z). It’s just an integer coordinate on a timeline. Converting it into “Tuesday 14:03 in Vienna” is a user-space job, and it happens in a few very specific steps.

6.1 Step 1 — split nanoseconds into seconds + remainder

Most time libraries work in “seconds since epoch” plus a fractional part:

  • sec = epoch_ns / 1_000_000_000
  • nsec = epoch_ns % 1_000_000_000

That split is practical: seconds are large-scale time, nanoseconds are the sub-second detail.

6.2 Step 2 — interpret seconds as UTC and create a UTC timestamp

At this point the number becomes “a moment” in UTC:

  • utc_instant = epoch_seconds_to_utc(sec, nsec)

6.3 Step 3 — convert UTC to a named time zone using tzdata rules

Now comes the real-world complexity.

A numeric offset like +01:00 is not a time zone. It’s just “the offset right now.” Real zones (like Europe/Vienna) are a ruleset: they include historical changes and DST transitions.

So the conversion is:

  • local_instant = convert_utc_to_zone(utc_instant, "Europe/Vienna", tzdata)

That conversion does three things:

  1. finds the correct offset for that instant (+01:00 or +02:00, depending on DST and history)
  2. applies that offset
  3. produces local calendar fields (year/month/day/hour/min/sec) plus the offset

This is why “time zones are formatting” is wrong: it’s not string styling, it’s rule evaluation.

6.4 Ambiguous and missing local times (DST pain in one minute)

DST creates two special situations that break naive systems:

Ambiguous local time (fall back)

The clock repeats an hour. The same local time occurs twice.

  • Example: 2026-10-25 02:30 in many European zones can mean two different instants.

Missing local time (spring forward)

The clock jumps forward. Some local times never occur.

  • Example: 02:30 on the spring-forward day might not exist at all.

Notice what happens here: converting from UTC → local is always unambiguous (UTC instants are unique). The pain happens when converting from local → UTC without enough context.

That’s why systems that store “local wall time” without a zone ID eventually end up in a fight with reality.

6.5 Step 4 — format for display or transport (ISO 8601 / RFC 3339)

After conversion, formatting is easy:

  • UTC canonical log style: 2026-03-05T13:03:12.123456789Z
  • Local display style (with offset): 2026-03-05T14:03:12.123456789+01:00

The important thing is that formatted output should preserve:

  • the offset (or Z)
  • and ideally the zone context when it matters

6.6 What should be stored vs what should be displayed

This is where many systems accidentally create “time debt.”

Store (internally / in DB / across services):

  • an unambiguous instant:
    • epoch timestamp (integer + unit), or
    • UTC/RFC3339 timestamp with Z

Display (UI / reports):

  • convert to the user’s zone at the edge using tzdata.

If civil meaning matters (schedules, payroll, appointments):

  • store the rule, not just the instant:

    • “every day at 09:00 Europe/Vienna”
    • plus the zone ID

      Because recurring human schedules live in civil time and DST rules matter.

6.7 Tiny practical checklist (saves a lot of bugs)

  • Use a zone ID (Europe/Vienna), not a fixed offset, for civil-time logic.
  • Keep timestamps in UTC-like canonical form internally.
  • Convert to local time only at the edges.
  • Treat “local naive timestamps” as incomplete data unless paired with a zone/ruleset.
  • When parsing timestamps, require either:
    • Z, or
    • an explicit offset, or
    • a zone ID (for civil-time workflows).

7) Compact model (matches the diagram)

Variables

  • tsc_last, mult, shift
  • mono (ns since boot)
  • suspend_ns (ns spent suspended)
  • wall_off (epoch ns − mono)

Update step (on each read)

  • compute delta ticks from the clocksource
  • convert delta ticks → delta ns
  • accumulate monotonic time: mono += delta_ns

Clock readouts

  • CLOCK_MONOTONIC = mono
  • CLOCK_BOOTTIME = mono + suspend_ns
  • CLOCK_REALTIME = mono + wall_off (setting time changes wall_off, not mono)

8) Code: a small Linux-style emulator

I tried to create an clear and easy to understand code that would nicely show how everything works together.

This code mirrors the diagram and uses Linux-like APIs (clock_gettime(CLOCK_...), clock_settime(CLOCK_REALTIME, ...)) to show the interactions between RTC, TSC, and the kernel clocks.

You can find the result of my experiment here:

https://github.com/DmytroHuzz/linux_clock_emulator/blob/main/linux_clock.py


9) Developer cheat sheet

  • Use CLOCK_MONOTONIC for: timeouts, retries, intervals, measuring latency, scheduling “sleep X”.
  • Use CLOCK_BOOTTIME for elapsed time that should include suspend.
  • Use CLOCK_REALTIME for logs, audits, UI timestamps, business meaning.
  • Never compute durations as realtime_end - realtime_start.
  • Time zone conversion is userspace logic (tzdata). Store UTC-like timestamps internally.

Next: Part 3

Part 3 leaves the single machine and goes to the network and we will see how to sync many machines.

Top comments (0)